mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 14:10:44 +00:00
Compare commits
223 Commits
version-0.
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
d977abfc3d | |||
c559cc21eb | |||
8ebe776847 | |||
eefbdd212f | |||
9e9d2926c6 | |||
994ab15652 | |||
164087beb0 | |||
070dec1266 | |||
27c9694433 | |||
a95e9d06a2 | |||
b9b32d4b7d | |||
1c99ea2c6f | |||
f19cd26892 | |||
253e93c356 | |||
3e8afde744 | |||
ce823ad22f | |||
93e1c01939 | |||
c37eead6be | |||
d9458ced34 | |||
01a19180b9 | |||
406069768b | |||
83055e1784 | |||
2e0d1dbb1c | |||
a899db4b3e | |||
1a52e3354e | |||
c912fe7cc2 | |||
0aa0f0a085 | |||
4b9d7b135b | |||
7082f7c2b1 | |||
d28249e5ce | |||
7da34d70da | |||
0d0ab6c2e5 | |||
2577bbeb37 | |||
db0b434e52 | |||
d4e18d8f55 | |||
f2e674ded4 | |||
366639c882 | |||
dbaad85ec7 | |||
8479ef4b90 | |||
1da85ce0a2 | |||
5dfa10912e | |||
05f2341099 | |||
3406929e75 | |||
c548952281 | |||
d811ed725d | |||
4cb00670ed | |||
4cbd808a55 | |||
fc17ef6106 | |||
b01023e541 | |||
a4df67f9f9 | |||
b4a63e6e1b | |||
0d78555a05 | |||
6a1db1c533 | |||
fabde303ab | |||
6d173d5485 | |||
79ac48b7f9 | |||
f7c8513845 | |||
be30422c18 | |||
53c005051e | |||
3879d5412c | |||
d596346c44 | |||
ac88def08c | |||
29bb7252e8 | |||
f0bd6b4b80 | |||
726075f041 | |||
67a1afdc1a | |||
c832028961 | |||
774309e846 | |||
a19a516fc0 | |||
e153d3a7b4 | |||
34a1755e44 | |||
b4a2ba9582 | |||
9bb6600a9d | |||
4f081e7c79 | |||
a0a43d48f5 | |||
7c4e06abcc | |||
39d714fbb1 | |||
6975069049 | |||
f05759ae83 | |||
81e4d5d36d | |||
bff8a0f78a | |||
87fbbc797a | |||
742caba8d2 | |||
f6f5a07d3a | |||
bc50c1e22e | |||
71672d46ee | |||
75c84cd0ff | |||
0ff98ce7ea | |||
c53f5e935d | |||
f5f2cc5007 | |||
d62a4c6daf | |||
367fa4f657 | |||
5a5c9a0072 | |||
2db1adc9ab | |||
3d2708da23 | |||
6ec838c48b | |||
ebb7e7ab45 | |||
8c3648920d | |||
6d6510c223 | |||
2422d09c31 | |||
c04ab42bd9 | |||
c003d448d6 | |||
de8d668ea2 | |||
953d97c554 | |||
086787a5ca | |||
e6816174c3 | |||
ed60ad2a96 | |||
a943628e84 | |||
5cfcb420d0 | |||
593c9f25f9 | |||
ca560c933d | |||
51dd0b6b29 | |||
b924fdcffa | |||
5b84df1752 | |||
9af1fed422 | |||
4299199947 | |||
6f578eb2f0 | |||
be5b8f0869 | |||
c27723e956 | |||
cbff44f6a3 | |||
9167f356e9 | |||
dccefb0a40 | |||
e24f15e2d8 | |||
d0a0f93933 | |||
76d226b5e8 | |||
2bf611b24f | |||
56b9738f15 | |||
40d073bf54 | |||
c0297dea91 | |||
9e4743e8d8 | |||
976f173c93 | |||
3f74594c69 | |||
da08382055 | |||
22a9e4b06b | |||
c086874e64 | |||
75ae848afd | |||
53b74fb61d | |||
3ae938ac6e | |||
905ac6d72a | |||
a6af75ebdc | |||
2039c727b5 | |||
c38b8ac595 | |||
3187706967 | |||
ffdb392b9f | |||
f79940d4c3 | |||
be14afc11c | |||
360bf56481 | |||
b4507ba431 | |||
57675528b5 | |||
3fae35a2bf | |||
688d104635 | |||
d533e59a84 | |||
184ef8db47 | |||
a62a1f10cd | |||
a056c4618c | |||
ed22ab5917 | |||
598e26d9a1 | |||
b53d1823df | |||
6bc543fff7 | |||
e28f8b8317 | |||
9d1266036c | |||
b93032aae4 | |||
300af89687 | |||
059dffb620 | |||
1822dd8189 | |||
b75fd29a57 | |||
5ff91739d4 | |||
ada95431c1 | |||
c24b7cd84b | |||
bf80eb1845 | |||
db0c490a32 | |||
a73bd40c6f | |||
7eea0c08bb | |||
18c11d3869 | |||
a367ec717c | |||
65e8f7a71f | |||
900484c01c | |||
6bfe0cd3ea | |||
12d1de7135 | |||
e5efe8a64c | |||
9d3cd50c7b | |||
5ae18c31f9 | |||
ad11bf64db | |||
3a69957b43 | |||
7af5ad2655 | |||
57569e4c84 | |||
3791c069c7 | |||
0f799339d6 | |||
3e113a12b3 | |||
79304adcb0 | |||
2c192a3fd9 | |||
3772cf725f | |||
1cf83fb441 | |||
4efb818bfe | |||
f30b2a9ef8 | |||
2efc732530 | |||
1ed51e0a35 | |||
54bf5bed8f | |||
12eb3be3cb | |||
4105f034aa | |||
a38d57c7af | |||
14fa7fdb74 | |||
c07c007bc2 | |||
9fe5067374 | |||
599aff3487 | |||
59b7e0529c | |||
d9a98c5987 | |||
b58f93e627 | |||
1c1a0f99e8 | |||
d273d091df | |||
d0194d9580 | |||
54a0d6720f | |||
19ee5bcb23 | |||
eee7354e77 | |||
6b590324be | |||
a340299c67 | |||
6a01754479 | |||
4ccc726f2e | |||
eb151f0610 | |||
a18d2afee5 | |||
0007909792 | |||
78ba2b591c | |||
0b525f8775 |
@ -5,8 +5,8 @@ if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ]; then
|
||||
echo "On a primary branch [$CIRCLE_BRANCH] - will not edit the pom version.";
|
||||
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ] || [ \! -z $(echo "$CIRCLE_TAG" | grep "^version-") ]; then
|
||||
echo "On a primary branch or tag [${CIRCLE_BRANCH}${CIRCLE_TAG}] - will not edit the pom version.";
|
||||
exit 0;
|
||||
fi
|
||||
|
||||
|
@ -10,7 +10,7 @@ The bundle contains all of the sub-jars. It is named:
|
||||
|
||||
```qqq-${version}.jar```
|
||||
|
||||
You can also use fine grained jars:
|
||||
You can also use fine-grained jars:
|
||||
- `qqq-backend-core`: The core module. Useful if you're developing other modules.
|
||||
- `qqq-backend-module-rdbms`: Backend module for working with Relational Databases.
|
||||
- `qqq-backend-module-filesystem`: Backend module for working with Filesystems (including AWS S3).
|
||||
@ -35,4 +35,3 @@ GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
|
2
pom.xml
2
pom.xml
@ -44,7 +44,7 @@
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>0.14.0</revision>
|
||||
<revision>0.19.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
|
||||
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
|
||||
import com.kingsrook.qqq.backend.core.state.StateType;
|
||||
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
@ -50,6 +51,7 @@ public class AsyncJobManager
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
|
||||
|
||||
private String forcedJobUUID = null;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -69,7 +71,8 @@ public class AsyncJobManager
|
||||
*******************************************************************************/
|
||||
public <T extends Serializable> T startJob(String jobName, long timeout, TimeUnit timeUnit, AsyncJob<T> asyncJob) throws JobGoingAsyncException, QException
|
||||
{
|
||||
UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.ASYNC_JOB_STATUS);
|
||||
UUID jobUUID = StringUtils.hasContent(forcedJobUUID) ? UUID.fromString(forcedJobUUID) : UUID.randomUUID();
|
||||
UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS);
|
||||
AsyncJobStatus asyncJobStatus = new AsyncJobStatus();
|
||||
asyncJobStatus.setState(AsyncJobState.RUNNING);
|
||||
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
|
||||
@ -205,4 +208,35 @@ public class AsyncJobManager
|
||||
jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for forcedJobUUID
|
||||
*******************************************************************************/
|
||||
public String getForcedJobUUID()
|
||||
{
|
||||
return (this.forcedJobUUID);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for forcedJobUUID
|
||||
*******************************************************************************/
|
||||
public void setForcedJobUUID(String forcedJobUUID)
|
||||
{
|
||||
this.forcedJobUUID = forcedJobUUID;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for forcedJobUUID
|
||||
*******************************************************************************/
|
||||
public AsyncJobManager withForcedJobUUID(String forcedJobUUID)
|
||||
{
|
||||
this.forcedJobUUID = forcedJobUUID;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.async;
|
||||
import java.io.Serializable;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
@ -142,6 +143,11 @@ public class AsyncRecordPipeLoop
|
||||
jobState = asyncJobStatus.getState();
|
||||
}
|
||||
|
||||
if(recordPipe instanceof BufferedRecordPipe bufferedRecordPipe)
|
||||
{
|
||||
bufferedRecordPipe.finalFlush();
|
||||
}
|
||||
|
||||
LOG.debug("Job [" + jobUUID + "][" + jobName + "] completed with status: " + asyncJobStatus);
|
||||
|
||||
///////////////////////////////////
|
||||
|
@ -29,6 +29,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
@ -44,10 +45,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -76,6 +79,21 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute to insert 1 audit, with a list of detail child records provided as just string messages
|
||||
*******************************************************************************/
|
||||
public static void executeWithStringDetails(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<String> detailMessages)
|
||||
{
|
||||
List<QRecord> detailRecords = null;
|
||||
if(CollectionUtils.nullSafeHasContents(detailMessages))
|
||||
{
|
||||
detailRecords = detailMessages.stream().map(m -> new QRecord().withValue("message", m)).toList();
|
||||
}
|
||||
execute(tableName, recordId, securityKeyValues, message, detailRecords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Execute to insert 1 audit, with a list of detail child records
|
||||
*******************************************************************************/
|
||||
@ -92,12 +110,26 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Simple overload that internally figures out primary key and security key values
|
||||
**
|
||||
** Be aware - if the record doesn't have its security key values set (say it's a
|
||||
** partial record as part of an update), then those values won't be in the
|
||||
** security key map... This should probably be considered a bug.
|
||||
*******************************************************************************/
|
||||
public static void appendToInput(AuditInput auditInput, QTableMetaData table, QRecord record, String auditMessage)
|
||||
{
|
||||
appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.empty()), auditMessage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Add 1 auditSingleInput to an AuditInput object - with no details (child records).
|
||||
*******************************************************************************/
|
||||
public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
|
||||
public static void appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
|
||||
{
|
||||
return (appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null));
|
||||
appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null);
|
||||
}
|
||||
|
||||
|
||||
@ -123,6 +155,44 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given record, from a given table, build a map of the record's security
|
||||
** key values.
|
||||
**
|
||||
** If, in case, the record has null value(s), and the oldRecord is given (e.g.,
|
||||
** for the case of an update, where the record may not have all fields set, and
|
||||
** oldRecord should be known for doing field-diffs), then try to get the value(s)
|
||||
** from oldRecord.
|
||||
**
|
||||
** Currently, will leave values null if they aren't found after that.
|
||||
**
|
||||
** An alternative could be to re-fetch the record from its source if needed...
|
||||
*******************************************************************************/
|
||||
public static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record, Optional<QRecord> oldRecord)
|
||||
{
|
||||
Map<String, Serializable> securityKeyValues = new HashMap<>();
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||
{
|
||||
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
|
||||
|
||||
if(keyValue == null && oldRecord.isPresent())
|
||||
{
|
||||
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
|
||||
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
if(keyValue == null)
|
||||
{
|
||||
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
|
||||
}
|
||||
|
||||
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
|
||||
}
|
||||
return securityKeyValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -151,7 +221,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
|
||||
///////////////////////////////////////////////////
|
||||
// validate security keys on the table are given //
|
||||
///////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||
{
|
||||
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
|
||||
{
|
||||
|
@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.audits;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
@ -52,12 +54,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.actions.audits.AuditAction.getRecordSecurityKeyValues;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -69,6 +71,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(DMLAuditAction.class);
|
||||
|
||||
public static final String AUDIT_CONTEXT_FIELD_NAME = "auditContext";
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -98,34 +102,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
return (output);
|
||||
}
|
||||
|
||||
String contextSuffix = "";
|
||||
if(StringUtils.hasContent(input.getAuditContext()))
|
||||
{
|
||||
contextSuffix = " " + input.getAuditContext();
|
||||
}
|
||||
|
||||
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
|
||||
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
|
||||
{
|
||||
String processName = runProcessInput.getProcessName();
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process != null)
|
||||
{
|
||||
contextSuffix = " during process: " + process.getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
QSession qSession = QContext.getQSession();
|
||||
String apiVersion = qSession.getValue("apiVersion");
|
||||
if(apiVersion != null)
|
||||
{
|
||||
String apiLabel = qSession.getValue("apiLabel");
|
||||
if(!StringUtils.hasContent(apiLabel))
|
||||
{
|
||||
apiLabel = "API";
|
||||
}
|
||||
contextSuffix += (" via " + apiLabel + " Version: " + apiVersion);
|
||||
}
|
||||
String contextSuffix = getContentSuffix(input);
|
||||
|
||||
AuditInput auditInput = new AuditInput();
|
||||
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
|
||||
@ -136,7 +113,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : recordList)
|
||||
{
|
||||
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix);
|
||||
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.empty()), "Record was " + dmlType.pastTenseVerb + contextSuffix);
|
||||
}
|
||||
}
|
||||
else if(auditLevel.equals(AuditLevel.FIELD))
|
||||
@ -146,7 +123,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// do many audits, all with field level details, for FIELD level //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), qSession);
|
||||
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
|
||||
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
|
||||
|
||||
//////////////////////////////////////////
|
||||
@ -168,92 +145,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
List<QRecord> details = new ArrayList<>();
|
||||
for(String fieldName : sortedFieldNames)
|
||||
{
|
||||
if(!record.getValues().containsKey(fieldName))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the stored record doesn't have this field name, then don't audit anything about it //
|
||||
// this is to deal with our Patch style updates not looking like every field was cleared out. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QFieldMetaData field = table.getField(fieldName);
|
||||
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
|
||||
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
|
||||
QRecord detailRecord = null;
|
||||
|
||||
if(oldRecord == null)
|
||||
{
|
||||
if(DMLType.INSERT.equals(dmlType) && value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(field.getType().equals(QFieldType.BLOB))
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!Objects.equals(oldValue, value))
|
||||
{
|
||||
if(field.getType().equals(QFieldType.BLOB))
|
||||
{
|
||||
if(oldValue == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
|
||||
}
|
||||
else if(value == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
|
||||
|
||||
if(oldValue == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
else if(value == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
|
||||
detailRecord.withValue("oldValue", formattedOldValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
|
||||
detailRecord.withValue("oldValue", formattedOldValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(detailRecord != null)
|
||||
{
|
||||
detailRecord.withValue("fieldName", fieldName);
|
||||
details.add(detailRecord);
|
||||
}
|
||||
makeAuditDetailRecordForField(fieldName, table, dmlType, record, oldRecord)
|
||||
.ifPresent(details::add);
|
||||
}
|
||||
|
||||
if(details.isEmpty() && DMLType.UPDATE.equals(dmlType))
|
||||
@ -263,7 +156,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
}
|
||||
else
|
||||
{
|
||||
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
|
||||
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.ofNullable(oldRecord)), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -283,6 +176,254 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static String getContentSuffix(DMLAuditInput input)
|
||||
{
|
||||
StringBuilder contextSuffix = new StringBuilder();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// start with context from the input wrapper //
|
||||
// note, these contexts get propagated down from Input/Update/Delete Input //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
if(StringUtils.hasContent(input.getAuditContext()))
|
||||
{
|
||||
contextSuffix.append(" ").append(input.getAuditContext());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// note process label (and a possible context from the process's state) if present //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
|
||||
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
|
||||
{
|
||||
String processAuditContext = ValueUtils.getValueAsString(runProcessInput.getValue(AUDIT_CONTEXT_FIELD_NAME));
|
||||
if(StringUtils.hasContent(processAuditContext))
|
||||
{
|
||||
contextSuffix.append(" ").append(processAuditContext);
|
||||
}
|
||||
|
||||
String processName = runProcessInput.getProcessName();
|
||||
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
|
||||
if(process != null)
|
||||
{
|
||||
contextSuffix.append(" during process: ").append(process.getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////
|
||||
// use api label & version if present in session //
|
||||
///////////////////////////////////////////////////
|
||||
QSession qSession = QContext.getQSession();
|
||||
String apiVersion = qSession.getValue("apiVersion");
|
||||
if(apiVersion != null)
|
||||
{
|
||||
String apiLabel = qSession.getValue("apiLabel");
|
||||
if(!StringUtils.hasContent(apiLabel))
|
||||
{
|
||||
apiLabel = "API";
|
||||
}
|
||||
contextSuffix.append(" via ").append(apiLabel).append(" Version: ").append(apiVersion);
|
||||
}
|
||||
return (contextSuffix.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static Optional<QRecord> makeAuditDetailRecordForField(String fieldName, QTableMetaData table, DMLType dmlType, QRecord record, QRecord oldRecord)
|
||||
{
|
||||
if(!record.getValues().containsKey(fieldName))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the stored record doesn't have this field name, then don't audit anything about it //
|
||||
// this is to deal with our Patch style updates not looking like every field was cleared out. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
QFieldMetaData field = table.getField(fieldName);
|
||||
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
|
||||
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
|
||||
QRecord detailRecord = null;
|
||||
|
||||
if(oldRecord == null)
|
||||
{
|
||||
if(DMLType.INSERT.equals(dmlType) && value == null)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(areValuesDifferentForAudit(field, value, oldValue))
|
||||
{
|
||||
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
|
||||
{
|
||||
if(oldValue == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
|
||||
}
|
||||
else if(value == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
|
||||
}
|
||||
else
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
|
||||
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
|
||||
|
||||
if(oldValue == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
else if(value == null)
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
|
||||
detailRecord.withValue("oldValue", formattedOldValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
|
||||
detailRecord.withValue("oldValue", formattedOldValue);
|
||||
detailRecord.withValue("newValue", formattedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(detailRecord != null)
|
||||
{
|
||||
LOG.debug("Returning with message: " + detailRecord.getValueString("message"));
|
||||
|
||||
detailRecord.withValue("fieldName", fieldName);
|
||||
return (Optional.of(detailRecord));
|
||||
}
|
||||
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static boolean areValuesDifferentForAudit(QFieldMetaData field, Serializable value, Serializable oldValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
///////////////////
|
||||
// decimal rules //
|
||||
///////////////////
|
||||
if(field.getType().equals(QFieldType.DECIMAL))
|
||||
{
|
||||
BigDecimal newBD = ValueUtils.getValueAsBigDecimal(value);
|
||||
BigDecimal oldBD = ValueUtils.getValueAsBigDecimal(oldValue);
|
||||
|
||||
if(newBD == null && oldBD == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(newBD == null || oldBD == null)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
return (newBD.compareTo(oldBD) != 0);
|
||||
}
|
||||
|
||||
////////////////////
|
||||
// dateTime rules //
|
||||
////////////////////
|
||||
if(field.getType().equals(QFieldType.DATE_TIME))
|
||||
{
|
||||
Instant newI = ValueUtils.getValueAsInstant(value);
|
||||
Instant oldI = ValueUtils.getValueAsInstant(oldValue);
|
||||
|
||||
if(newI == null && oldI == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(newI == null || oldI == null)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// just compare to the second //
|
||||
////////////////////////////////
|
||||
return (newI.truncatedTo(ChronoUnit.SECONDS).compareTo(oldI.truncatedTo(ChronoUnit.SECONDS)) != 0);
|
||||
}
|
||||
|
||||
//////////////////
|
||||
// string rules //
|
||||
//////////////////
|
||||
if(field.getType().isStringLike())
|
||||
{
|
||||
String newString = ValueUtils.getValueAsString(value);
|
||||
String oldString = ValueUtils.getValueAsString(oldValue);
|
||||
|
||||
boolean newIsNullOrEmpty = !StringUtils.hasContent(newString);
|
||||
boolean oldIsNullOrEmpty = !StringUtils.hasContent(oldString);
|
||||
|
||||
if(newIsNullOrEmpty && oldIsNullOrEmpty)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
if(newIsNullOrEmpty || oldIsNullOrEmpty)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
return (newString.compareTo(oldString) != 0);
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// default just use Objects.equals //
|
||||
/////////////////////////////////////
|
||||
return !Objects.equals(oldValue, value);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error checking areValuesDifferentForAudit", e, logPair("fieldName", field.getName()), logPair("value", value), logPair("oldValue", oldValue));
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// default to something simple... //
|
||||
////////////////////////////////////
|
||||
return !Objects.equals(oldValue, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -372,21 +513,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
|
||||
{
|
||||
Map<String, Serializable> securityKeyValues = new HashMap<>();
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
|
||||
}
|
||||
return securityKeyValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -406,7 +532,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private enum DMLType
|
||||
enum DMLType
|
||||
{
|
||||
INSERT("Inserted", true),
|
||||
UPDATE("Edited", true),
|
||||
|
@ -22,9 +22,8 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.automation;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
@ -90,6 +89,10 @@ public class RecordAutomationStatusUpdater
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Avoid setting records to PENDING_INSERT or PENDING_UPDATE even if they don't have any insert or update automations or triggers //
|
||||
// such records should go straight to OK status. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(canWeSkipPendingAndGoToOkay(table, automationStatus))
|
||||
{
|
||||
automationStatus = AutomationStatus.OK;
|
||||
@ -121,9 +124,13 @@ public class RecordAutomationStatusUpdater
|
||||
** being asked to set status to PENDING_INSERT (or PENDING_UPDATE), then just
|
||||
** move the status straight to OK.
|
||||
*******************************************************************************/
|
||||
private static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
|
||||
static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
|
||||
{
|
||||
List<TableAutomationAction> tableActions = Objects.requireNonNullElse(table.getAutomationDetails().getActions(), new ArrayList<>());
|
||||
List<TableAutomationAction> tableActions = Collections.emptyList();
|
||||
if(table.getAutomationDetails() != null && table.getAutomationDetails().getActions() != null)
|
||||
{
|
||||
tableActions = table.getAutomationDetails().getActions();
|
||||
}
|
||||
|
||||
if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS))
|
||||
{
|
||||
@ -135,6 +142,12 @@ public class RecordAutomationStatusUpdater
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're going to pending-insert, and there are no insert automations or triggers, //
|
||||
// then we may skip pending and go to okay. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (true);
|
||||
}
|
||||
else if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
|
||||
{
|
||||
@ -146,9 +159,21 @@ public class RecordAutomationStatusUpdater
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're going to pending-update, and there are no insert automations or triggers, //
|
||||
// then we may skip pending and go to okay. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (true);
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we're going to any other automation status - then we may never "skip pending" and go to okay - //
|
||||
// because we weren't asked to go to pending! //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
@ -74,7 +75,7 @@ public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(ScriptRevision.TABLE_NAME);
|
||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, scriptId)));
|
||||
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin("currentScriptRevision")));
|
||||
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME)));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
|
||||
{
|
||||
|
@ -39,12 +39,15 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
|
||||
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.QFilterOrderBy;
|
||||
@ -61,11 +64,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -270,16 +276,39 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
// todo - get filter if there is/was one
|
||||
rs.add(new TableAutomationAction()
|
||||
.withName("Script:" + record.getValue("scriptId"))
|
||||
.withFilter(null)
|
||||
.withTriggerEvent(triggerEvent)
|
||||
.withPriority(record.getValueInteger("priority"))
|
||||
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
|
||||
.withValues(MapBuilder.of("scriptId", record.getValue("scriptId")))
|
||||
.withIncludeRecordAssociations(true)
|
||||
);
|
||||
TableTrigger tableTrigger = new TableTrigger(record);
|
||||
|
||||
try
|
||||
{
|
||||
QQueryFilter filter = null;
|
||||
Integer filterId = tableTrigger.getFilterId();
|
||||
if(filterId != null)
|
||||
{
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(SavedFilter.TABLE_NAME);
|
||||
getInput.setPrimaryKey(filterId);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
if(getOutput.getRecord() != null)
|
||||
{
|
||||
SavedFilter savedFilter = new SavedFilter(getOutput.getRecord());
|
||||
filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class);
|
||||
}
|
||||
}
|
||||
|
||||
rs.add(new TableAutomationAction()
|
||||
.withName("Script:" + tableTrigger.getScriptId())
|
||||
.withFilter(filter)
|
||||
.withTriggerEvent(triggerEvent)
|
||||
.withPriority(tableTrigger.getPriority())
|
||||
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
|
||||
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
|
||||
.withIncludeRecordAssociations(true)
|
||||
);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error setting up table trigger", e, logPair("tableTriggerId", tableTrigger.getId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -313,22 +342,9 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
boolean anyActionsFailed = false;
|
||||
for(TableAutomationAction action : actions)
|
||||
{
|
||||
try
|
||||
boolean hadError = applyActionToRecords(table, records, action);
|
||||
if(hadError)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
|
||||
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
|
||||
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
|
||||
{
|
||||
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
|
||||
applyActionToMatchingRecords(table, matchingQRecords, action);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
|
||||
anyActionsFailed = true;
|
||||
}
|
||||
}
|
||||
@ -348,6 +364,37 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run one action over a list of records (if they match the action's filter).
|
||||
**
|
||||
** @return hadError - true if an exception was caught; false if all OK.
|
||||
*******************************************************************************/
|
||||
protected boolean applyActionToRecords(QTableMetaData table, List<QRecord> records, TableAutomationAction action)
|
||||
{
|
||||
try
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
|
||||
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
|
||||
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
|
||||
{
|
||||
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
|
||||
applyActionToMatchingRecords(table, matchingQRecords, action);
|
||||
}
|
||||
|
||||
return (false);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given action, and a list of records - return a new list, of the ones
|
||||
** which match the action's filter (if there is one - if not, then all match).
|
||||
|
@ -29,6 +29,7 @@ import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
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.update.UpdateInput;
|
||||
@ -42,18 +43,16 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
/*******************************************************************************
|
||||
** Standard/re-usable post-insert customizer, for the use case where, when we
|
||||
** do an insert into table "parent", we want a record automatically inserted into
|
||||
** table "child", and there's a foreign key in "parent", pointed at "child"
|
||||
** e.g., named: "parent.childId".
|
||||
** table "child". Optionally (based on RelationshipType), there can be a foreign
|
||||
** key in "parent", pointed at "child". e.g., named: "parent.childId".
|
||||
**
|
||||
** A similar use-case would have the foreign key in the child table - in which case,
|
||||
** we could add a "Type" enum, plus abstract method to get our "Type", then logic
|
||||
** to switch behavior based on type. See existing type enum, but w/ only 1 case :)
|
||||
*******************************************************************************/
|
||||
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
|
||||
{
|
||||
public enum RelationshipType
|
||||
{
|
||||
PARENT_POINTS_AT_CHILD
|
||||
PARENT_POINTS_AT_CHILD,
|
||||
CHILD_POINTS_AT_PARENT
|
||||
}
|
||||
|
||||
|
||||
@ -68,10 +67,17 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
|
||||
*******************************************************************************/
|
||||
public abstract String getChildTableName();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract String getForeignKeyFieldName();
|
||||
public String getForeignKeyFieldName()
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -88,7 +94,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
|
||||
{
|
||||
try
|
||||
{
|
||||
List<QRecord> rs = new ArrayList<>();
|
||||
List<QRecord> rs = records;
|
||||
List<QRecord> childrenToInsert = new ArrayList<>();
|
||||
QTableMetaData table = getInsertInput().getTable();
|
||||
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
|
||||
@ -97,12 +103,37 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
|
||||
// iterate over the inserted records, building a list child records to insert //
|
||||
// for ones missing a value in the foreign key field. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord record : records)
|
||||
switch(getRelationshipType())
|
||||
{
|
||||
if(record.getValue(getForeignKeyFieldName()) == null)
|
||||
case PARENT_POINTS_AT_CHILD ->
|
||||
{
|
||||
childrenToInsert.add(buildChildForRecord(record));
|
||||
String foreignKeyFieldName = getForeignKeyFieldName();
|
||||
try
|
||||
{
|
||||
table.getField(foreignKeyFieldName);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new QRuntimeException("For RelationshipType.PARENT_POINTS_AT_CHILD, a valid foreignKeyFieldName in the parent table must be given. "
|
||||
+ "[" + foreignKeyFieldName + "] is not a valid field name in table [" + table.getName() + "]");
|
||||
}
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(record.getValue(foreignKeyFieldName) == null)
|
||||
{
|
||||
childrenToInsert.add(buildChildForRecord(record));
|
||||
}
|
||||
}
|
||||
}
|
||||
case CHILD_POINTS_AT_PARENT ->
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
childrenToInsert.add(buildChildForRecord(record));
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + getRelationshipType());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
@ -129,51 +160,70 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// for the PARENT_POINTS_AT_CHILD relationship type:
|
||||
// iterate over the original list of records again - for any that need a child (e.g., are missing //
|
||||
// foreign key), set their foreign key to a newly inserted child's key, and add them to be updated. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
switch(getRelationshipType())
|
||||
{
|
||||
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(record.getValue(getForeignKeyFieldName()) == null)
|
||||
case PARENT_POINTS_AT_CHILD ->
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get the corresponding child record, if it has any errors, set that as a warning in the parent //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QRecord childRecord = insertedRecordIterator.next();
|
||||
if(CollectionUtils.nullSafeHasContents(childRecord.getErrors()))
|
||||
rs = new ArrayList<>();
|
||||
List<QRecord> recordsToUpdate = new ArrayList<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
for(QStatusMessage error : childRecord.getErrors())
|
||||
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(record.getValue(getForeignKeyFieldName()) == null)
|
||||
{
|
||||
record.addWarning(new QWarningMessage("Error creating child " + childTable.getLabel() + " (" + error.toString() + ")"));
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// get the corresponding child record, if it has any errors, set that as a warning in the parent //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QRecord childRecord = insertedRecordIterator.next();
|
||||
if(CollectionUtils.nullSafeHasContents(childRecord.getErrors()))
|
||||
{
|
||||
for(QStatusMessage error : childRecord.getErrors())
|
||||
{
|
||||
record.addWarning(new QWarningMessage("Error creating child " + childTable.getLabel() + " (" + error.toString() + ")"));
|
||||
}
|
||||
rs.add(record);
|
||||
continue;
|
||||
}
|
||||
|
||||
Serializable foreignKey = childRecord.getValue(childTable.getPrimaryKeyField());
|
||||
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
|
||||
record.setValue(getForeignKeyFieldName(), foreignKey);
|
||||
rs.add(record);
|
||||
}
|
||||
else
|
||||
{
|
||||
rs.add(record);
|
||||
}
|
||||
rs.add(record);
|
||||
continue;
|
||||
}
|
||||
|
||||
Serializable foreignKey = childRecord.getValue(childTable.getPrimaryKeyField());
|
||||
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
|
||||
record.setValue(getForeignKeyFieldName(), foreignKey);
|
||||
rs.add(record);
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// update the originally inserted records to reference their new children //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(getInsertInput().getTableName());
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
updateInput.setTransaction(this.insertInput.getTransaction());
|
||||
new UpdateAction().execute(updateInput);
|
||||
}
|
||||
else
|
||||
case CHILD_POINTS_AT_PARENT ->
|
||||
{
|
||||
rs.add(record);
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - some version of looking at the inserted children to confirm that they were inserted, and updating the parents with warnings if they weren't //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
default -> throw new IllegalStateException("Unexpected value: " + getRelationshipType());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// update the originally inserted records to reference their new children //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(getInsertInput().getTableName());
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
updateInput.setTransaction(this.insertInput.getTransaction());
|
||||
new UpdateAction().execute(updateInput);
|
||||
|
||||
return (rs);
|
||||
}
|
||||
catch(RuntimeException re)
|
||||
{
|
||||
throw (re);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw new RuntimeException("Error inserting new child records for new parent records", e);
|
||||
|
@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOu
|
||||
** Interface for the Aggregate action.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface AggregateInterface
|
||||
public interface AggregateInterface extends BaseQueryInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.interfaces;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base class for "query" (e.g., read-operations) action interfaces (query, count, aggregate).
|
||||
** Initially just here for the QueryStat methods - if we expand those to apply
|
||||
** to insert/update/delete, well, then rename this maybe to BaseActionInterface?
|
||||
*******************************************************************************/
|
||||
public interface BaseQueryInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void setQueryStat(QueryStat queryStat)
|
||||
{
|
||||
//////////
|
||||
// noop //
|
||||
//////////
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default QueryStat getQueryStat()
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void setQueryStatFirstResultTime()
|
||||
{
|
||||
QueryStat queryStat = getQueryStat();
|
||||
if(queryStat != null)
|
||||
{
|
||||
if(queryStat.getFirstResultTimestamp() == null)
|
||||
{
|
||||
queryStat.setFirstResultTimestamp(Instant.now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void cancelAction()
|
||||
{
|
||||
//////////////////////////////////////////////
|
||||
// initially at least, a noop in base class //
|
||||
//////////////////////////////////////////////
|
||||
}
|
||||
|
||||
}
|
@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
** Interface for the Count action.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface CountInterface
|
||||
public interface CountInterface extends BaseQueryInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
|
@ -31,10 +31,11 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
** Interface for the Query action.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface QueryInterface
|
||||
public interface QueryInterface extends BaseQueryInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
QueryOutput execute(QueryInput queryInput) throws QException;
|
||||
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRend
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
|
||||
@ -50,6 +51,7 @@ 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.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
@ -57,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
|
||||
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
|
||||
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
|
||||
@ -483,6 +486,35 @@ public class RunProcessAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected String determineBasepullKeyValue(QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
|
||||
{
|
||||
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(process.getSchedule() != null && process.getSchedule().getVariantBackend() != null)
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getSchedule().getVariantBackend());
|
||||
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
|
||||
{
|
||||
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
|
||||
}
|
||||
else
|
||||
{
|
||||
basepullKeyValue += "-" + session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue());
|
||||
}
|
||||
}
|
||||
|
||||
return (basepullKeyValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert or update the last runtime value for this basepull into the backend.
|
||||
*******************************************************************************/
|
||||
@ -491,7 +523,7 @@ public class RunProcessAction
|
||||
String basepullTableName = basepullConfiguration.getTableName();
|
||||
String basepullKeyFieldName = basepullConfiguration.getKeyField();
|
||||
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
|
||||
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
|
||||
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
|
||||
|
||||
///////////////////////////////////////
|
||||
// get the stored basepull timestamp //
|
||||
@ -571,7 +603,7 @@ public class RunProcessAction
|
||||
String basepullKeyFieldName = basepullConfiguration.getKeyField();
|
||||
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
|
||||
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
|
||||
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
|
||||
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
|
||||
|
||||
///////////////////////////////////////
|
||||
// get the stored basepull timestamp //
|
||||
|
@ -197,7 +197,27 @@ public class ExportAction
|
||||
String joinTableName = parts[0];
|
||||
if(!addedJoinNames.contains(joinTableName))
|
||||
{
|
||||
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
|
||||
QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true);
|
||||
queryJoins.add(queryJoin);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
|
||||
// This code basically mirrors what QFMD is doing right now, so it's better - //
|
||||
// but shouldn't all of this just be in JoinsContext? it does some of this... //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
Optional<ExposedJoin> exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
|
||||
if(exposedJoinOptional.isEmpty())
|
||||
{
|
||||
throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
|
||||
}
|
||||
ExposedJoin exposedJoin = exposedJoinOptional.get();
|
||||
|
||||
if(exposedJoin.getJoinPath().size() == 1)
|
||||
{
|
||||
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
|
||||
}
|
||||
|
||||
addedJoinNames.add(joinTableName);
|
||||
}
|
||||
}
|
||||
|
@ -23,22 +23,35 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
|
||||
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.data.QRecord;
|
||||
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.scripts.ScriptRevision;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -101,6 +114,22 @@ public class ExecuteCodeAction
|
||||
context.putAll(input.getInput());
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// safely always set the deploymentMode //
|
||||
//////////////////////////////////////////
|
||||
context.put("deploymentMode", ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getDeploymentMode(), null));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// set the qCodeExecutor into any context objects which are QCodeExecutorAware //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
for(Serializable value : context.values())
|
||||
{
|
||||
if(value instanceof QCodeExecutorAware qCodeExecutorAware)
|
||||
{
|
||||
qCodeExecutorAware.setQCodeExecutor(qCodeExecutor);
|
||||
}
|
||||
}
|
||||
|
||||
Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger);
|
||||
output.setOutput(codeOutput);
|
||||
executionLogger.acceptExecutionEnd(codeOutput);
|
||||
@ -122,7 +151,17 @@ public class ExecuteCodeAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision)
|
||||
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision) throws QException
|
||||
{
|
||||
return setupExecuteCodeInput(input, scriptRevision, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision, String fileName) throws QException
|
||||
{
|
||||
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
|
||||
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
|
||||
@ -139,7 +178,49 @@ public class ExecuteCodeAction
|
||||
context.put("scriptUtils", input.getScriptUtils());
|
||||
}
|
||||
|
||||
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
|
||||
if(CollectionUtils.nullSafeIsEmpty(scriptRevision.getFiles()))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(ScriptRevisionFile.TABLE_NAME);
|
||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, scriptRevision.getId())));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
scriptRevision.setFiles(new ArrayList<>());
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
scriptRevision.getFiles().add(new ScriptRevisionFile(record));
|
||||
}
|
||||
}
|
||||
|
||||
List<ScriptRevisionFile> files = scriptRevision.getFiles();
|
||||
if(files == null || files.isEmpty())
|
||||
{
|
||||
throw (new QException("Script Revision " + scriptRevision.getId() + " had more than 1 associated ScriptRevisionFile (and the name to use was not specified)."));
|
||||
}
|
||||
else
|
||||
{
|
||||
String contents = null;
|
||||
if(fileName == null || files.size() == 1)
|
||||
{
|
||||
contents = files.get(0).getContents();
|
||||
}
|
||||
else
|
||||
{
|
||||
for(ScriptRevisionFile file : files)
|
||||
{
|
||||
if(file.getFileName().equals(fileName))
|
||||
{
|
||||
contents = file.getContents();
|
||||
}
|
||||
}
|
||||
if(contents == null)
|
||||
{
|
||||
throw (new QException("Could not find file named " + fileName + " for Script Revision " + scriptRevision.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(contents).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
|
||||
}
|
||||
|
||||
ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision);
|
||||
context.put("qqq", new QqqScriptUtils());
|
||||
@ -157,7 +238,19 @@ public class ExecuteCodeAction
|
||||
*******************************************************************************/
|
||||
public static void addApiUtilityToContext(Map<String, Serializable> context, ScriptRevision scriptRevision)
|
||||
{
|
||||
if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion()))
|
||||
addApiUtilityToContext(context, scriptRevision.getApiName(), scriptRevision.getApiVersion());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Try to (dynamically) load the ApiScriptUtils object from the api middleware
|
||||
** module -- in case the runtime doesn't have that module deployed (e.g, not in
|
||||
** the project pom).
|
||||
*******************************************************************************/
|
||||
public static void addApiUtilityToContext(Map<String, Serializable> context, String apiName, String apiVersion)
|
||||
{
|
||||
if(!StringUtils.hasContent(apiName) || !StringUtils.hasContent(apiVersion))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -165,7 +258,7 @@ public class ExecuteCodeAction
|
||||
try
|
||||
{
|
||||
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
|
||||
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(scriptRevision.getApiName(), scriptRevision.getApiVersion());
|
||||
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(apiName, apiVersion);
|
||||
context.put("api", (Serializable) apiScriptUtilsObject);
|
||||
}
|
||||
catch(ClassNotFoundException e)
|
||||
|
@ -41,4 +41,24 @@ public interface QCodeExecutor
|
||||
*******************************************************************************/
|
||||
Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
|
||||
|
||||
/*******************************************************************************
|
||||
** Process an object from the script's language/runtime into a (more) native java object.
|
||||
** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such
|
||||
**
|
||||
*******************************************************************************/
|
||||
default Object convertObjectToJava(Object object) throws QCodeException
|
||||
{
|
||||
return (object);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Convert a native java object into one for the script's language/runtime.
|
||||
** e.g., a java Instant to a Nashorn Date
|
||||
**
|
||||
*******************************************************************************/
|
||||
default Object convertJavaObject(Object object, Object requestedTypeHint) throws QCodeException
|
||||
{
|
||||
return (object);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.scripts;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for classes that can accept a QCodeExecutor object via a setter.
|
||||
*******************************************************************************/
|
||||
public interface QCodeExecutorAware
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void setQCodeExecutor(QCodeExecutor qCodeExecutor);
|
||||
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.scripts;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
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.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class RecordScriptTestInterface implements TestScriptActionInterface
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput) throws QException
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void execute(TestScriptInput input, TestScriptOutput output) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
Serializable scriptId = input.getInputValues().get("scriptId");
|
||||
QRecord script = new GetAction().executeForRecord(new GetInput(Script.TABLE_NAME).withPrimaryKey(scriptId));
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// look up the records being tested against //
|
||||
//////////////////////////////////////////////
|
||||
String tableName = script.getValueString("tableName");
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QException("Could not find table [" + tableName + "] for script"));
|
||||
}
|
||||
|
||||
String recordPrimaryKeyList = ValueUtils.getValueAsString(input.getInputValues().get("recordPrimaryKeyList"));
|
||||
if(!StringUtils.hasContent(recordPrimaryKeyList))
|
||||
{
|
||||
throw (new QException("Record primary key list was not given."));
|
||||
}
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(tableName)
|
||||
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))))
|
||||
.withIncludeAssociations(true));
|
||||
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
|
||||
{
|
||||
throw (new QException("No records were found by the given primary keys."));
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// set up & run the action //
|
||||
/////////////////////////////
|
||||
RunAdHocRecordScriptInput runAdHocRecordScriptInput = new RunAdHocRecordScriptInput();
|
||||
runAdHocRecordScriptInput.setRecordList(queryOutput.getRecords());
|
||||
|
||||
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
|
||||
runAdHocRecordScriptInput.setLogger(executionLogger);
|
||||
|
||||
runAdHocRecordScriptInput.setTableName(tableName);
|
||||
runAdHocRecordScriptInput.setCodeReference((AdHocScriptCodeReference) input.getCodeReference());
|
||||
RunAdHocRecordScriptOutput runAdHocRecordScriptOutput = new RunAdHocRecordScriptOutput();
|
||||
new RunAdHocRecordScriptAction().run(runAdHocRecordScriptInput, runAdHocRecordScriptOutput);
|
||||
|
||||
/////////////////////////////////
|
||||
// send outputs back to caller //
|
||||
/////////////////////////////////
|
||||
output.setScriptLog(executionLogger.getScriptLog());
|
||||
output.setScriptLogLines(executionLogger.getScriptLogLines());
|
||||
if(runAdHocRecordScriptOutput.getException().isPresent())
|
||||
{
|
||||
output.setException(runAdHocRecordScriptOutput.getException().get());
|
||||
}
|
||||
}
|
||||
catch(QException e)
|
||||
{
|
||||
output.setException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QFieldMetaData> getTestInputFields()
|
||||
{
|
||||
return (List.of(new QFieldMetaData("recordPrimaryKeyList", QFieldType.STRING).withLabel("Record Primary Key List")));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QFieldMetaData> getTestOutputFields()
|
||||
{
|
||||
return (Collections.emptyList());
|
||||
}
|
||||
|
||||
}
|
@ -51,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReferen
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
@ -197,7 +198,7 @@ public class RunAdHocRecordScriptAction
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(ScriptRevision.TABLE_NAME);
|
||||
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("script.id", QCriteriaOperator.EQUALS, codeReference.getScriptId())));
|
||||
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin("currentScriptRevision")));
|
||||
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME)));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
|
||||
|
@ -110,6 +110,7 @@ public class RunAssociatedScriptAction
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName("scriptRevision");
|
||||
getInput.setPrimaryKey(scriptRevisionId);
|
||||
getInput.setIncludeAssociations(true);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
|
@ -31,6 +31,8 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
@ -47,6 +49,7 @@ 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.tables.AssociatedScript;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.StoreScriptRevisionProcessStep;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
@ -180,41 +183,19 @@ public class StoreAssociatedScriptAction
|
||||
}
|
||||
}
|
||||
|
||||
QRecord scriptRevision = new QRecord()
|
||||
.withValue("scriptId", script.getValue("id"))
|
||||
.withValue("contents", input.getCode())
|
||||
.withValue("apiName", input.getApiName())
|
||||
.withValue("apiVersion", input.getApiVersion())
|
||||
.withValue("commitMessage", commitMessage)
|
||||
.withValue("sequenceNo", nextSequenceNo);
|
||||
|
||||
try
|
||||
{
|
||||
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
scriptRevision.setValue("author", "Unknown");
|
||||
}
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("scriptRevision");
|
||||
insertInput.setRecords(List.of(scriptRevision));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
scriptRevision = insertOutput.getRecords().get(0);
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// update the script to point at the new revision //
|
||||
////////////////////////////////////////////////////
|
||||
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName("script");
|
||||
updateInput.setRecords(List.of(script));
|
||||
new UpdateAction().execute(updateInput);
|
||||
RunBackendStepInput storeScriptRevisionInput = new RunBackendStepInput();
|
||||
storeScriptRevisionInput.addValue("scriptId", script.getValue("id"));
|
||||
storeScriptRevisionInput.addValue("commitMessage", commitMessage);
|
||||
storeScriptRevisionInput.addValue("apiName", input.getApiName());
|
||||
storeScriptRevisionInput.addValue("apiVersion", input.getApiVersion());
|
||||
storeScriptRevisionInput.addValue("fileNames", "script");
|
||||
storeScriptRevisionInput.addValue("fileContents:script", input.getCode());
|
||||
RunBackendStepOutput storeScriptRevisionOutput = new RunBackendStepOutput();
|
||||
new StoreScriptRevisionProcessStep().run(storeScriptRevisionInput, storeScriptRevisionOutput);
|
||||
|
||||
output.setScriptId(script.getValueInteger("id"));
|
||||
output.setScriptName(script.getValueString("name"));
|
||||
output.setScriptRevisionId(scriptRevision.getValueInteger("id"));
|
||||
output.setScriptRevisionSequenceNo(scriptRevision.getValueInteger("sequenceNo"));
|
||||
output.setScriptRevisionId(storeScriptRevisionOutput.getValueInteger("scriptRevisionId"));
|
||||
output.setScriptRevisionSequenceNo(storeScriptRevisionOutput.getValueInteger("scriptRevisionSequenceNo"));
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
private QCodeReference qCodeReference;
|
||||
private String uuid = UUID.randomUUID().toString();
|
||||
|
||||
private boolean includeUUID = true;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -52,7 +53,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
this.qCodeReference = executeCodeInput.getCodeReference();
|
||||
|
||||
String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
|
||||
LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
|
||||
LOG.info("Starting script execution: " + qCodeReference.getName() + (includeUUID ? ", uuid: " + uuid : "") + ", with input: " + inputString);
|
||||
}
|
||||
|
||||
|
||||
@ -63,7 +64,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
@Override
|
||||
public void acceptLogLine(String logLine)
|
||||
{
|
||||
LOG.info("Script log: " + uuid + ": " + logLine);
|
||||
LOG.info("Script log: " + (includeUUID ? uuid + ": " : "") + logLine);
|
||||
}
|
||||
|
||||
|
||||
@ -74,7 +75,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
@Override
|
||||
public void acceptException(Exception exception)
|
||||
{
|
||||
LOG.info("Script Exception: " + uuid, exception);
|
||||
LOG.info("Script Exception: " + (includeUUID ? uuid : ""), exception);
|
||||
}
|
||||
|
||||
|
||||
@ -86,7 +87,38 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
public void acceptExecutionEnd(Serializable output)
|
||||
{
|
||||
String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "...");
|
||||
LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString);
|
||||
LOG.info("Finished script execution: " + qCodeReference.getName() + (includeUUID ? ", uuid: " + uuid : "") + ", with output: " + outputString);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for includeUUID
|
||||
*******************************************************************************/
|
||||
public boolean getIncludeUUID()
|
||||
{
|
||||
return (this.includeUUID);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for includeUUID
|
||||
*******************************************************************************/
|
||||
public void setIncludeUUID(boolean includeUUID)
|
||||
{
|
||||
this.includeUUID = includeUUID;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for includeUUID
|
||||
*******************************************************************************/
|
||||
public Log4jCodeExecutionLogger withIncludeUUID(boolean includeUUID)
|
||||
{
|
||||
this.includeUUID = includeUUID;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
/*******************************************************************************
|
||||
** Interface to provide logging functionality to QCodeExecution (e.g., scripts)
|
||||
*******************************************************************************/
|
||||
public interface QCodeExecutionLoggerInterface
|
||||
public interface QCodeExecutionLoggerInterface extends Serializable
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Implementation of a code execution logger that logs to System.out and
|
||||
** System.err (for exceptions)
|
||||
*******************************************************************************/
|
||||
public class SystemOutExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
{
|
||||
private QCodeReference qCodeReference;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
|
||||
{
|
||||
this.qCodeReference = executeCodeInput.getCodeReference();
|
||||
|
||||
String inputString = ValueUtils.getValueAsString(executeCodeInput.getInput());
|
||||
System.out.println("Starting script execution: " + qCodeReference.getName() + ", with input: " + inputString);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptLogLine(String logLine)
|
||||
{
|
||||
System.out.println("Script log: " + logLine);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptException(Exception exception)
|
||||
{
|
||||
System.out.println("Script Exception: " + exception.getMessage());
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptExecutionEnd(Serializable output)
|
||||
{
|
||||
String outputString = ValueUtils.getValueAsString(output);
|
||||
System.out.println("Finished script execution: " + qCodeReference.getName() + ", with output: " + outputString);
|
||||
}
|
||||
|
||||
}
|
@ -23,9 +23,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
|
||||
@ -36,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
*******************************************************************************/
|
||||
public class AggregateAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AggregateAction.class);
|
||||
|
||||
private AggregateInterface aggregateInterface;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -43,11 +55,36 @@ public class AggregateAction
|
||||
{
|
||||
ActionHelper.validateSession(aggregateInput);
|
||||
|
||||
QTableMetaData table = aggregateInput.getTable();
|
||||
QBackendMetaData backend = aggregateInput.getBackend();
|
||||
|
||||
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, aggregateInput.getFilter());
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
|
||||
// todo pre-customization - just get to modify the request?
|
||||
AggregateOutput aggregateOutput = qModule.getAggregateInterface().execute(aggregateInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
aggregateInterface = qModule.getAggregateInterface();
|
||||
aggregateInterface.setQueryStat(queryStat);
|
||||
AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput);
|
||||
|
||||
QueryStatManager.getInstance().add(queryStat);
|
||||
|
||||
return aggregateOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void cancel()
|
||||
{
|
||||
if(aggregateInterface == null)
|
||||
{
|
||||
LOG.warn("aggregateInterface object was null when requested to cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
aggregateInterface.cancelAction();
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
|
||||
@ -36,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
*******************************************************************************/
|
||||
public class CountAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CountAction.class);
|
||||
|
||||
private CountInterface countInterface;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -43,11 +55,36 @@ public class CountAction
|
||||
{
|
||||
ActionHelper.validateSession(countInput);
|
||||
|
||||
QTableMetaData table = countInput.getTable();
|
||||
QBackendMetaData backend = countInput.getBackend();
|
||||
|
||||
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, countInput.getFilter());
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
|
||||
// todo pre-customization - just get to modify the request?
|
||||
CountOutput countOutput = qModule.getCountInterface().execute(countInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
|
||||
|
||||
countInterface = qModule.getCountInterface();
|
||||
countInterface.setQueryStat(queryStat);
|
||||
CountOutput countOutput = countInterface.execute(countInput);
|
||||
|
||||
QueryStatManager.getInstance().add(queryStat);
|
||||
|
||||
return countOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void cancel()
|
||||
{
|
||||
if(countInterface == null)
|
||||
{
|
||||
LOG.warn("countInterface object was null when requested to cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
countInterface.cancelAction();
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,10 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCusto
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
@ -117,12 +119,14 @@ public class DeleteAction
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's a query filter, but the interface doesn't support using a query filter, then do a query for the filter, to get a list of primary keys instead //
|
||||
// or - anytime there are associations on the table we want primary keys, as that's what the manage associations method uses //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
|
||||
if(deleteInput.getQueryFilter() != null && (!deleteInterface.supportsQueryFilterInput() || CollectionUtils.nullSafeHasContents(table.getAssociations())))
|
||||
{
|
||||
LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes");
|
||||
LOG.info("Querying for primary keys, for table " + table.getName() + " in backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes (or the table has associations)");
|
||||
List<Serializable> primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
|
||||
deleteInput.setPrimaryKeys(primaryKeyList);
|
||||
primaryKeys = primaryKeyList;
|
||||
|
||||
if(primaryKeyList.isEmpty())
|
||||
{
|
||||
@ -165,10 +169,22 @@ public class DeleteAction
|
||||
|
||||
if(!primaryKeysToRemoveFromInput.isEmpty())
|
||||
{
|
||||
primaryKeys.removeAll(primaryKeysToRemoveFromInput);
|
||||
if(primaryKeys == null)
|
||||
{
|
||||
LOG.warn("There were primary keys to remove from the input, but no primary key list (filter supplied as input?)", new LogPair("primaryKeysToRemoveFromInput", primaryKeysToRemoveFromInput));
|
||||
}
|
||||
else
|
||||
{
|
||||
primaryKeys.removeAll(primaryKeysToRemoveFromInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// stash a copy of primary keys that didn't have errors (for use in manageAssociations below) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Set<Serializable> primaryKeysWithoutErrors = new HashSet<>(CollectionUtils.nonNullList(primaryKeys));
|
||||
|
||||
////////////////////////////////////
|
||||
// have the backend do the delete //
|
||||
////////////////////////////////////
|
||||
@ -187,11 +203,13 @@ public class DeleteAction
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if a record had a validation warning, but then an execution error, remove it from the warning list - so it's only in one of them. //
|
||||
// also, always remove from
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QRecord outputRecordWithError : outputRecordsWithErrors)
|
||||
{
|
||||
Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName);
|
||||
recordsWithValidationWarnings.remove(pkey);
|
||||
primaryKeysWithoutErrors.remove(pkey);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -211,15 +229,23 @@ public class DeleteAction
|
||||
////////////////////////////////////////
|
||||
// delete associations, if applicable //
|
||||
////////////////////////////////////////
|
||||
manageAssociations(deleteInput);
|
||||
manageAssociations(primaryKeysWithoutErrors, deleteInput);
|
||||
|
||||
///////////////////////////////////
|
||||
// do the audit //
|
||||
// todo - add input.omitDmlAudit //
|
||||
///////////////////////////////////
|
||||
DMLAuditInput dmlAuditInput = new DMLAuditInput().withTableActionInput(deleteInput);
|
||||
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
|
||||
new DMLAuditAction().execute(dmlAuditInput);
|
||||
//////////////////
|
||||
// do the audit //
|
||||
//////////////////
|
||||
if(deleteInput.getOmitDmlAudit())
|
||||
{
|
||||
LOG.debug("Requested to omit DML audit");
|
||||
}
|
||||
else
|
||||
{
|
||||
DMLAuditInput dmlAuditInput = new DMLAuditInput()
|
||||
.withTableActionInput(deleteInput)
|
||||
.withAuditContext(deleteInput.getAuditContext());
|
||||
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
|
||||
new DMLAuditAction().execute(dmlAuditInput);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
// finally, run the post-delete customizer, if there is one //
|
||||
@ -296,6 +322,8 @@ public class DeleteAction
|
||||
QTableMetaData table = deleteInput.getTable();
|
||||
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
|
||||
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// after all validations, run the pre-delete customizer, if there is one //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -340,7 +368,7 @@ public class DeleteAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void manageAssociations(DeleteInput deleteInput) throws QException
|
||||
private void manageAssociations(Set<Serializable> primaryKeysWithoutErrors, DeleteInput deleteInput) throws QException
|
||||
{
|
||||
QTableMetaData table = deleteInput.getTable();
|
||||
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
|
||||
@ -353,7 +381,7 @@ public class DeleteAction
|
||||
|
||||
if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys()));
|
||||
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, new ArrayList<>(primaryKeysWithoutErrors)));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -23,8 +23,6 @@ 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;
|
||||
@ -33,35 +31,24 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCusto
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.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.modules.backend.implementations.utils.BackendQueryFilterUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -70,8 +57,6 @@ import org.apache.commons.lang.NotImplementedException;
|
||||
*******************************************************************************/
|
||||
public class GetAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
|
||||
|
||||
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
|
||||
|
||||
private GetInput getInput;
|
||||
@ -79,6 +64,16 @@ public class GetAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRecord executeForRecord(GetInput getInput) throws QException
|
||||
{
|
||||
return (execute(getInput).getRecord());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -125,36 +120,7 @@ public class GetAction
|
||||
////////////////////////////
|
||||
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);
|
||||
if(recordFromSource != null)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// good, we found a record from the source, make sure we should cache it, and if so, do it now //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
|
||||
boolean shouldCacheRecord = shouldCacheRecord(table, recordToCache);
|
||||
if(shouldCacheRecord)
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
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);
|
||||
}
|
||||
new GetActionCacheHelper().handleCaching(getInput, getOutput);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
@ -171,168 +137,12 @@ public class GetAction
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
** Run a GetAction by using the QueryAction instead (e.g., with a filter made
|
||||
** from the pkey/ukey, and returning the single record if found).
|
||||
*******************************************************************************/
|
||||
private boolean shouldCacheRecord(QTableMetaData table, QRecord recordToCache)
|
||||
public GetOutput executeViaQuery(GetInput getInput) throws QException
|
||||
{
|
||||
boolean shouldCacheRecord = true;
|
||||
recordMatchExclusionLoop:
|
||||
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
|
||||
{
|
||||
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
|
||||
{
|
||||
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
|
||||
{
|
||||
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
|
||||
shouldCacheRecord = false;
|
||||
break recordMatchExclusionLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (shouldCacheRecord);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
|
||||
{
|
||||
QRecord cacheRecord = new QRecord(recordFromSource);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure every value in the qRecord is set, because we will possibly be doing an update //
|
||||
// on this record and want to null out any fields not set, not leave them populated //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String fieldName : table.getFields().keySet())
|
||||
{
|
||||
if(!cacheRecord.getValues().containsKey(fieldName))
|
||||
{
|
||||
cacheRecord.setValue(fieldName, null);
|
||||
}
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
|
||||
{
|
||||
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
|
||||
}
|
||||
return (cacheRecord);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
|
||||
{
|
||||
QTableMetaData table = getInput.getTable();
|
||||
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
|
||||
if(expirationSeconds != null)
|
||||
{
|
||||
QRecord cachedRecord = getOutput.getRecord();
|
||||
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
|
||||
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// keep the serial key from the old record in case we need to delete it //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
Serializable oldRecordPrimaryKey = getOutput.getRecord().getValue(table.getPrimaryKeyField());
|
||||
boolean shouldDeleteCachedRecord = true;
|
||||
|
||||
///////////////////////////////////////////
|
||||
// fetch record from original source now //
|
||||
///////////////////////////////////////////
|
||||
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
|
||||
if(recordFromSource != null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// if the record was found in the source, put it into the output //
|
||||
// object so returned back to caller, check that it should actually //
|
||||
// be cached before doing so //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
|
||||
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
|
||||
getOutput.setRecord(recordToCache);
|
||||
|
||||
if(shouldCacheRecord(table, recordToCache))
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(getInput.getTableName());
|
||||
updateInput.setRecords(List.of(recordToCache));
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
getOutput.setRecord(updateOutput.getRecords().get(0));
|
||||
shouldDeleteCachedRecord = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we did not get a record back from the source, empty out the getOutput's record //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
getOutput.setRecord(null);
|
||||
}
|
||||
|
||||
if(shouldDeleteCachedRecord)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// if the record is no longer in the source, then remove it from the cache //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(getInput.getTableName());
|
||||
deleteInput.setPrimaryKeys(List.of(oldRecordPrimaryKey));
|
||||
new DeleteAction().execute(deleteInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QRecord tryToGetFromCacheSource(GetInput getInput) 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();
|
||||
sourceGetInput.setTableName(sourceTableName);
|
||||
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
|
||||
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
|
||||
QRecord outputRecord = sourceGetOutput.getRecord();
|
||||
|
||||
return (outputRecord);
|
||||
return (new DefaultGetInterface().execute(getInput));
|
||||
}
|
||||
|
||||
|
||||
@ -345,42 +155,7 @@ public class GetAction
|
||||
@Override
|
||||
public GetOutput execute(GetInput getInput) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(getInput.getTableName());
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// build filter using either pkey or unique key //
|
||||
//////////////////////////////////////////////////
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
if(getInput.getPrimaryKey() != null)
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
|
||||
}
|
||||
else if(getInput.getUniqueKey() != null)
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
|
||||
{
|
||||
if(entry.getValue() == null)
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
|
||||
}
|
||||
|
||||
queryInput.setFilter(filter);
|
||||
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
|
||||
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
|
||||
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
|
||||
queryInput.setShouldMaskPasswords(getInput.getShouldMaskPasswords());
|
||||
queryInput.setShouldOmitHiddenFields(getInput.getShouldOmitHiddenFields());
|
||||
QueryInput queryInput = convertGetInputToQueryInput(getInput);
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
@ -395,6 +170,48 @@ public class GetAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QueryInput convertGetInputToQueryInput(GetInput getInput) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(getInput.getTableName());
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// build filter using either pkey or unique key //
|
||||
//////////////////////////////////////////////////
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
if(getInput.getPrimaryKey() != null)
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
|
||||
}
|
||||
else if(getInput.getUniqueKey() != null)
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
|
||||
{
|
||||
if(entry.getValue() == null)
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
|
||||
}
|
||||
|
||||
queryInput.setFilter(filter);
|
||||
queryInput.setCommonParamsFrom(getInput);
|
||||
return queryInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Run the necessary actions on a record. This may include setting display values,
|
||||
** translating possible values, and running post-record customizations.
|
||||
|
@ -78,6 +78,28 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRecord executeForRecord(InsertInput insertInput) throws QException
|
||||
{
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
return (insertOutput.getRecords().get(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> executeForRecords(InsertInput insertInput) throws QException
|
||||
{
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
return (insertOutput.getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -133,7 +155,10 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
}
|
||||
else
|
||||
{
|
||||
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
|
||||
new DMLAuditAction().execute(new DMLAuditInput()
|
||||
.withTableActionInput(insertInput)
|
||||
.withAuditContext(insertInput.getAuditContext())
|
||||
.withRecordList(insertOutput.getRecords()));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
@ -34,8 +34,11 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryActionCacheHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
@ -47,12 +50,14 @@ 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.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
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.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
||||
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;
|
||||
@ -71,6 +76,7 @@ public class QueryAction
|
||||
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
|
||||
|
||||
private QueryInput queryInput;
|
||||
private QueryInterface queryInterface;
|
||||
private QPossibleValueTranslator qPossibleValueTranslator;
|
||||
|
||||
|
||||
@ -87,12 +93,14 @@ public class QueryAction
|
||||
throw (new QException("Table name was not specified in query input"));
|
||||
}
|
||||
|
||||
if(queryInput.getTable() == null)
|
||||
QTableMetaData table = queryInput.getTable();
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
|
||||
}
|
||||
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
QBackendMetaData backend = queryInput.getBackend();
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
this.queryInput = queryInput;
|
||||
|
||||
if(queryInput.getRecordPipe() != null)
|
||||
@ -109,11 +117,24 @@ public class QueryAction
|
||||
}
|
||||
}
|
||||
|
||||
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, queryInput.getFilter());
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
|
||||
// todo pre-customization - just get to modify the request?
|
||||
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
|
||||
|
||||
queryInterface = qModule.getQueryInterface();
|
||||
queryInterface.setQueryStat(queryStat);
|
||||
QueryOutput queryOutput = queryInterface.execute(queryInput);
|
||||
|
||||
QueryStatManager.getInstance().add(queryStat);
|
||||
|
||||
////////////////////////////
|
||||
// handle cache use-cases //
|
||||
////////////////////////////
|
||||
if(table.getCacheOf() != null)
|
||||
{
|
||||
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
|
||||
}
|
||||
|
||||
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
|
||||
{
|
||||
@ -319,4 +340,20 @@ public class QueryAction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void cancel()
|
||||
{
|
||||
if(queryInterface == null)
|
||||
{
|
||||
LOG.warn("queryInterface object was null when requested to cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
queryInterface.cancelAction();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||
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.replace.ReplaceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceOutput;
|
||||
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.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Action to do a "replace" - e.g: Update rows with unique-key values that are
|
||||
** already in the table; insert rows whose unique keys weren't already in the
|
||||
** table, and delete rows that weren't in the input (all based on a
|
||||
** UniqueKey that's part of the input)
|
||||
**
|
||||
** Note - the filter in the ReplaceInput - its role is to limit what rows are
|
||||
** potentially deleted. e.g., if you have a table that's segmented, and you're
|
||||
** only replacing a particular segment of it (say, for 1 client), then you pass
|
||||
** in a filter that finds rows matching that segment. See Test for example.
|
||||
*******************************************************************************/
|
||||
public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, ReplaceOutput>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(ReplaceAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public ReplaceOutput execute(ReplaceInput input) throws QException
|
||||
{
|
||||
ReplaceOutput output = new ReplaceOutput();
|
||||
|
||||
QBackendTransaction transaction = input.getTransaction();
|
||||
boolean weOwnTheTransaction = false;
|
||||
|
||||
try
|
||||
{
|
||||
QTableMetaData table = input.getTable();
|
||||
UniqueKey uniqueKey = input.getKey();
|
||||
String primaryKeyField = table.getPrimaryKeyField();
|
||||
if(transaction == null)
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(input.getTableName());
|
||||
transaction = new InsertAction().openTransaction(insertInput);
|
||||
weOwnTheTransaction = true;
|
||||
}
|
||||
|
||||
List<QRecord> insertList = new ArrayList<>();
|
||||
List<QRecord> updateList = new ArrayList<>();
|
||||
List<Serializable> primaryKeysToKeep = new ArrayList<>();
|
||||
|
||||
for(List<QRecord> page : CollectionUtils.getPages(input.getRecords(), 1000))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// originally it was thought that we'd need to pass the filter in here //
|
||||
// but, it's been decided not to. the filter only applies to what we can delete //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey);
|
||||
for(QRecord record : page)
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
||||
if(keyValues.isPresent())
|
||||
{
|
||||
if(existingKeys.containsKey(keyValues.get()))
|
||||
{
|
||||
Serializable primaryKey = existingKeys.get(keyValues.get());
|
||||
record.setValue(primaryKeyField, primaryKey);
|
||||
updateList.add(record);
|
||||
primaryKeysToKeep.add(primaryKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
insertList.add(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(table.getName());
|
||||
insertInput.setRecords(insertList);
|
||||
insertInput.setTransaction(transaction);
|
||||
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
primaryKeysToKeep.addAll(insertOutput.getRecords().stream().map(r -> r.getValue(primaryKeyField)).toList());
|
||||
output.setInsertOutput(insertOutput);
|
||||
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(table.getName());
|
||||
updateInput.setRecords(updateList);
|
||||
updateInput.setTransaction(transaction);
|
||||
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
output.setUpdateOutput(updateOutput);
|
||||
|
||||
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
|
||||
if(input.getFilter() != null)
|
||||
{
|
||||
deleteFilter.addSubFilter(input.getFilter());
|
||||
}
|
||||
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(table.getName());
|
||||
deleteInput.setQueryFilter(deleteFilter);
|
||||
deleteInput.setTransaction(transaction);
|
||||
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
|
||||
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||
output.setDeleteOutput(deleteOutput);
|
||||
|
||||
if(weOwnTheTransaction)
|
||||
{
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
return (output);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(weOwnTheTransaction)
|
||||
{
|
||||
LOG.warn("Caught top-level ReplaceAction exception - rolling back exception", e);
|
||||
transaction.rollback();
|
||||
}
|
||||
throw (new QException("Error executing replace action", e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(weOwnTheTransaction)
|
||||
{
|
||||
transaction.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -61,6 +61,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
@ -83,6 +85,28 @@ public class UpdateAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRecord executeForRecord(UpdateInput updateInput) throws QException
|
||||
{
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
return (updateOutput.getRecords().get(0));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static List<QRecord> executeForRecords(UpdateInput updateInput) throws QException
|
||||
{
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
return (updateOutput.getRecords());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -187,14 +211,16 @@ public class UpdateAction
|
||||
{
|
||||
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
}
|
||||
|
||||
if(updateInput.getInputSource().shouldValidateRequiredFields())
|
||||
{
|
||||
validateRequiredFields(updateInput);
|
||||
}
|
||||
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// after all validations, run the pre-update customizer, if there is one //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
@ -265,6 +291,8 @@ public class UpdateAction
|
||||
QTableMetaData table = updateInput.getTable();
|
||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||
|
||||
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
|
||||
|
||||
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
|
||||
{
|
||||
List<Serializable> primaryKeysToLookup = new ArrayList<>();
|
||||
@ -298,6 +326,8 @@ public class UpdateAction
|
||||
}
|
||||
}
|
||||
|
||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
|
||||
for(QRecord record : page)
|
||||
{
|
||||
Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField()));
|
||||
@ -310,6 +340,19 @@ public class UpdateAction
|
||||
{
|
||||
record.addError(new NotFoundStatusMessage("No record was found to update for " + primaryKeyField.getLabel() + " = " + value));
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the table has any write-only locks, validate their values here, on the old-records //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock lock : onlyWriteLocks)
|
||||
{
|
||||
QRecord oldRecord = lookedUpRecords.get(value);
|
||||
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
|
||||
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
|
||||
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -374,6 +417,11 @@ public class UpdateAction
|
||||
//////////////////////////////////////////////////////
|
||||
for(QRecord record : page)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -0,0 +1,109 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For actions that may want to set a timeout, and cancel themselves if they run
|
||||
** too long - this class helps.
|
||||
**
|
||||
** Construct with the timeout (delay & timeUnit), and a runnable that takes care
|
||||
** of doing the cancel (e.g., cancelling a JDBC statement).
|
||||
**
|
||||
** Call start() to make a future get scheduled (note, if delay was null or <= 0,
|
||||
** then it doesn't get scheduled at all).
|
||||
**
|
||||
** Call cancel() if the action got far enough/completed, to cancel the future.
|
||||
**
|
||||
** You can check didTimeout (getDidTimeout()) to know if the timeout did occur.
|
||||
*******************************************************************************/
|
||||
public class ActionTimeoutHelper
|
||||
{
|
||||
private final Integer delay;
|
||||
private final TimeUnit timeUnit;
|
||||
private final Runnable runnable;
|
||||
private ScheduledFuture<?> future;
|
||||
|
||||
private boolean didTimeout = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ActionTimeoutHelper(Integer delay, TimeUnit timeUnit, Runnable runnable)
|
||||
{
|
||||
this.delay = delay;
|
||||
this.timeUnit = timeUnit;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void start()
|
||||
{
|
||||
if(delay == null || delay <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
future = Executors.newSingleThreadScheduledExecutor().schedule(() ->
|
||||
{
|
||||
didTimeout = true;
|
||||
runnable.run();
|
||||
}, delay, timeUnit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void cancel()
|
||||
{
|
||||
if(future != null)
|
||||
{
|
||||
future.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for didTimeout
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getDidTimeout()
|
||||
{
|
||||
return didTimeout;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class CacheUtils
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(CacheUtils.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource, CacheUseCase cacheUseCase)
|
||||
{
|
||||
QRecord cacheRecord = new QRecord(recordFromSource);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// make sure every value in the qRecord is set, because we will possibly be doing an update //
|
||||
// on this record and want to null out any fields not set, not leave them populated //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(String fieldName : table.getFields().keySet())
|
||||
{
|
||||
if(fieldName.equals(table.getPrimaryKeyField()))
|
||||
{
|
||||
if(!cacheUseCase.getDoCopySourcePrimaryKeyToCache())
|
||||
{
|
||||
cacheRecord.removeValue(fieldName);
|
||||
}
|
||||
}
|
||||
else if(!cacheRecord.getValues().containsKey(fieldName))
|
||||
{
|
||||
cacheRecord.setValue(fieldName, null);
|
||||
}
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
|
||||
{
|
||||
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
|
||||
}
|
||||
return (cacheRecord);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static boolean shouldCacheRecord(QTableMetaData table, QRecord recordToCache)
|
||||
{
|
||||
boolean shouldCacheRecord = true;
|
||||
recordMatchExclusionLoop:
|
||||
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
|
||||
{
|
||||
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
|
||||
{
|
||||
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
|
||||
{
|
||||
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
|
||||
shouldCacheRecord = false;
|
||||
break recordMatchExclusionLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (shouldCacheRecord);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class GetActionCacheHelper
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(GetActionCacheHelper.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void handleCaching(GetInput getInput, GetOutput getOutput) throws QException
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// copy Get input & output into Query input & output //
|
||||
///////////////////////////////////////////////////////
|
||||
QueryInput queryInput = GetAction.convertGetInputToQueryInput(getInput);
|
||||
|
||||
QueryOutput queryOutput = new QueryOutput(queryInput);
|
||||
if(getOutput.getRecord() != null)
|
||||
{
|
||||
queryOutput.addRecord(getOutput.getRecord());
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// run the QueryActionCacheHelper //
|
||||
////////////////////////////////////
|
||||
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
|
||||
|
||||
///////////////////////////////////
|
||||
// set result back in get output //
|
||||
///////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
|
||||
{
|
||||
getOutput.setRecord(queryOutput.getRecords().get(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
getOutput.setRecord(null);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// In July 2023, initial caching was added in QueryAction. //
|
||||
// at this time, it felt wrong to essentially duplicate this code between Get & Query - as Get is a simplified use-case of Query. //
|
||||
// so - we'll keep this code here, as a potential quick/easy fallback - but - see above - where we use QueryActionCacheHelper instead. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
public void handleCaching(GetInput getInput, GetOutput getOutput) throws QException
|
||||
{
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if the record wasn't found, see if we should look in cache-source //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
|
||||
if(recordFromSource != null)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// good, we found a record from the source, make sure we should cache it, and if so, do it now //
|
||||
// note, we always return the record from the source, even if we don't cache it. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = getInput.getTable();
|
||||
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, recordFromSource);
|
||||
getOutput.setRecord(recordToCache);
|
||||
|
||||
boolean shouldCacheRecord = CacheUtils.shouldCacheRecord(table, recordToCache);
|
||||
if(shouldCacheRecord)
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(getInput.getTableName());
|
||||
insertInput.setRecords(List.of(recordToCache));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// update the result record from the insert (e.g., so we get its id, just in case we care) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private QRecord tryToGetFromCacheSource(GetInput getInput) 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 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)))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// keep the serial key from the old record in case we need to delete it //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
Serializable oldRecordPrimaryKey = cachedRecord.getValue(table.getPrimaryKeyField());
|
||||
boolean shouldDeleteCachedRecord;
|
||||
|
||||
///////////////////////////////////////////
|
||||
// fetch record from original source now //
|
||||
///////////////////////////////////////////
|
||||
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
|
||||
if(recordFromSource != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// if the record was found in the source, put it into the output //
|
||||
// object so returned back to caller //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, recordFromSource);
|
||||
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
|
||||
getOutput.setRecord(recordToCache);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the record should be cached, update the cache record - else set the flag to delete the cached record. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(CacheUtils.shouldCacheRecord(table, recordToCache))
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(getInput.getTableName());
|
||||
updateInput.setRecords(List.of(recordToCache));
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
getOutput.setRecord(updateOutput.getRecords().get(0));
|
||||
shouldDeleteCachedRecord = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldDeleteCachedRecord = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we did not get a record back from the source, empty out the getOutput's record //
|
||||
// and set the flag to delete the cached record //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
getOutput.setRecord(null);
|
||||
shouldDeleteCachedRecord = true;
|
||||
}
|
||||
|
||||
if(shouldDeleteCachedRecord)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the record is no longer in the source (or it was in the source, but failed the should-cache check), then remove it from the cache //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(getInput.getTableName());
|
||||
deleteInput.setPrimaryKeys(List.of(oldRecordPrimaryKey));
|
||||
new DeleteAction().execute(deleteInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////
|
||||
// do a Get on the source table, by the unique key //
|
||||
/////////////////////////////////////////////////////
|
||||
GetInput sourceGetInput = new GetInput();
|
||||
sourceGetInput.setTableName(sourceTableName);
|
||||
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
|
||||
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
|
||||
QRecord outputRecord = sourceGetOutput.getRecord();
|
||||
|
||||
return (outputRecord);
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
@ -0,0 +1,622 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.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.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** After running a query, if it's for a table that's a CacheOf another table,
|
||||
** see if there are any cache use-cases to apply to the query result.
|
||||
**
|
||||
** Such as:
|
||||
** - if it's a query for one or more values in a UniqueKey:
|
||||
** - if any particular UniqueKeys weren't found, look in the source table
|
||||
** - if any cached records are expired, refresh them from the source
|
||||
** - possibly updating the cached record; possibly deleting it.
|
||||
*******************************************************************************/
|
||||
public class QueryActionCacheHelper
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QueryActionCacheHelper.class);
|
||||
|
||||
private boolean isQueryInputCacheable = false;
|
||||
private Map<CacheUseCase.Type, CacheUseCase> cacheUseCaseMap = new HashMap<>();
|
||||
private CacheUseCase activeCacheUseCase = null;
|
||||
|
||||
private UniqueKey cacheUniqueKey = null;
|
||||
private ListingHash<String, Serializable> uniqueKeyValues = new ListingHash<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void handleCaching(QueryInput queryInput, QueryOutput queryOutput) throws QException
|
||||
{
|
||||
analyzeInput(queryInput);
|
||||
if(!isQueryInputCacheable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// figure out which keys in the query were found, and which were missed //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> recordsFoundInCache = new ArrayList<>(queryOutput.getRecords());
|
||||
Set<List<Serializable>> uniqueKeyValuesInFoundRecords = getUniqueKeyValuesFromFoundRecords(queryOutput.getRecords());
|
||||
Set<List<Serializable>> missedUniqueKeyValues = getUniqueKeyValuesFromQuery();
|
||||
missedUniqueKeyValues.removeAll(uniqueKeyValuesInFoundRecords);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if any requested records weren't found, see if we should look in cache-source //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(missedUniqueKeyValues))
|
||||
{
|
||||
List<QRecord> recordsFromSource = tryToGetFromCacheSource(queryInput, missedUniqueKeyValues);
|
||||
if(CollectionUtils.nullSafeHasContents(recordsFromSource))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// good, we found records from the source, make sure we should cache them, and if so, do it now //
|
||||
// note, we always return the record from the source, even if we don't cache it. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = queryInput.getTable();
|
||||
|
||||
List<QRecord> recordsToReturn = recordsFromSource.stream()
|
||||
.map(r -> CacheUtils.mapSourceRecordToCacheRecord(table, r, activeCacheUseCase))
|
||||
.toList();
|
||||
queryOutput.addRecords(recordsToReturn);
|
||||
|
||||
List<QRecord> recordsToCache = recordsToReturn.stream()
|
||||
.filter(r -> CacheUtils.shouldCacheRecord(table, r))
|
||||
.toList();
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(recordsToCache))
|
||||
{
|
||||
try
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(queryInput.getTableName());
|
||||
insertInput.setRecords(recordsToCache);
|
||||
insertInput.setSkipUniqueKeyCheck(true);
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// set the (generated) ids in the records being returne //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<List<Serializable>, QRecord> insertedRecordsByUniqueKey = new HashMap<>();
|
||||
for(QRecord record : insertOutput.getRecords())
|
||||
{
|
||||
insertedRecordsByUniqueKey.put(getUniqueKeyValues(record), record);
|
||||
}
|
||||
for(QRecord record : recordsToReturn)
|
||||
{
|
||||
QRecord insertedRecord = insertedRecordsByUniqueKey.get(getUniqueKeyValues(record));
|
||||
if(insertedRecord != null)
|
||||
{
|
||||
record.setValue(table.getPrimaryKeyField(), insertedRecord.getValue(table.getPrimaryKeyField()));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// don't let an exception break this query - it (probably) just indicates some data that didn't get cached - so - that's generally "ok" //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Error inserting cached records", e, logPair("cacheTable", queryInput.getTableName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// for records that were found, if they're too old, maybe re-fetch them //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(recordsFoundInCache))
|
||||
{
|
||||
refreshCacheIfExpired(recordsFoundInCache, queryInput, queryOutput);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void refreshCacheIfExpired(List<QRecord> recordsFoundInCache, QueryInput queryInput, QueryOutput queryOutput) throws QException
|
||||
{
|
||||
QTableMetaData table = queryInput.getTable();
|
||||
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
|
||||
|
||||
if(expirationSeconds != null)
|
||||
{
|
||||
List<QRecord> expiredRecords = new ArrayList<>();
|
||||
for(QRecord cachedRecord : recordsFoundInCache)
|
||||
{
|
||||
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
|
||||
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
|
||||
{
|
||||
expiredRecords.add(cachedRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(expiredRecords))
|
||||
{
|
||||
Map<List<Serializable>, Serializable> uniqueKeyToPrimaryKeyMap = getUniqueKeyToPrimaryKeyMap(table.getPrimaryKeyField(), expiredRecords);
|
||||
Set<List<Serializable>> uniqueKeyValuesToRefresh = uniqueKeyToPrimaryKeyMap.keySet();
|
||||
|
||||
////////////////////////////////////////////
|
||||
// fetch records from original source now //
|
||||
////////////////////////////////////////////
|
||||
List<QRecord> recordsFromSource = tryToGetFromCacheSource(queryInput, uniqueKeyValuesToRefresh);
|
||||
|
||||
Set<List<Serializable>> uniqueKeyValuesInFoundRecords = getUniqueKeyValuesFromFoundRecords(recordsFromSource);
|
||||
Set<List<Serializable>> missedUniqueKeyValues = getUniqueKeyValuesFromQuery();
|
||||
missedUniqueKeyValues.retainAll(getUniqueKeyValuesFromFoundRecords(expiredRecords));
|
||||
missedUniqueKeyValues.removeAll(uniqueKeyValuesInFoundRecords);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// build records to cache - setting their original (from cache) ids back in them, so they'll update //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> refreshedRecordsToReturn = recordsFromSource.stream()
|
||||
.map(r ->
|
||||
{
|
||||
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, r, activeCacheUseCase);
|
||||
recordToCache.setValue(table.getPrimaryKeyField(), uniqueKeyToPrimaryKeyMap.get(getUniqueKeyValues(recordToCache)));
|
||||
return (recordToCache);
|
||||
})
|
||||
.toList();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the records were found in the source, put it into the output object so returned back to caller //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Map<List<Serializable>, QRecord> refreshedRecordsByUniqueKeyValues = refreshedRecordsToReturn.stream().collect(Collectors.toMap(this::getUniqueKeyValues, r -> r, (a, b) -> a));
|
||||
|
||||
ListIterator<QRecord> queryOutputListIterator = queryOutput.getRecords().listIterator();
|
||||
while(queryOutputListIterator.hasNext())
|
||||
{
|
||||
QRecord originalRecord = queryOutputListIterator.next();
|
||||
List<Serializable> recordUniqueKeyValues = getUniqueKeyValues(originalRecord);
|
||||
QRecord refreshedRecord = refreshedRecordsByUniqueKeyValues.get(recordUniqueKeyValues);
|
||||
|
||||
if(refreshedRecord != null)
|
||||
{
|
||||
queryOutputListIterator.set(refreshedRecord);
|
||||
}
|
||||
else if(missedUniqueKeyValues.contains(recordUniqueKeyValues))
|
||||
{
|
||||
queryOutputListIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// for refreshed records which should be cached, update them in the cache //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> recordsToUpdate = refreshedRecordsToReturn.stream().filter(r -> CacheUtils.shouldCacheRecord(table, r)).toList();
|
||||
if(CollectionUtils.nullSafeHasContents(recordsToUpdate))
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(queryInput.getTableName());
|
||||
updateInput.setRecords(recordsToUpdate);
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// don't let an exception break this query - it (probably) just indicates some data that didn't get cached - so - that's generally "ok" //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Error updating cached records", e, logPair("cacheTable", queryInput.getTableName()));
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the records were missed in the source - OR if they shouldn't be cached now, then mark them for deleting //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Set<Serializable> cachedRecordIdsToDelete = missedUniqueKeyValues.stream()
|
||||
.map(uniqueKeyToPrimaryKeyMap::get)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
cachedRecordIdsToDelete.addAll(refreshedRecordsToReturn.stream()
|
||||
.filter(r -> !CacheUtils.shouldCacheRecord(table, r))
|
||||
.map(r -> r.getValue(table.getPrimaryKeyField()))
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(cachedRecordIdsToDelete))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// if the records are no longer in the source, then remove them from the cache //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(queryInput.getTableName());
|
||||
deleteInput.setPrimaryKeys(new ArrayList<>(cachedRecordIdsToDelete));
|
||||
new DeleteAction().execute(deleteInput);
|
||||
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// don't let an exception break this query - it (probably) just indicates some data that didn't get uncached - so - that's generally "ok" //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.warn("Error deleting cached records", e, logPair("cacheTable", queryInput.getTableName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<List<Serializable>> getUniqueKeyValuesFromQuery()
|
||||
{
|
||||
Set<List<Serializable>> rs = new HashSet<>();
|
||||
|
||||
int noOfUniqueKeys = uniqueKeyValues.get(cacheUniqueKey.getFieldNames().get(0)).size();
|
||||
for(int i = 0; i < noOfUniqueKeys; i++)
|
||||
{
|
||||
List<Serializable> values = new ArrayList<>();
|
||||
|
||||
for(String fieldName : cacheUniqueKey.getFieldNames())
|
||||
{
|
||||
values.add(uniqueKeyValues.get(fieldName).get(i));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// critical - leave this here so hashCode from the list is correctly computed //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
rs.add(values);
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<List<Serializable>> getUniqueKeyValuesFromFoundRecords(List<QRecord> records)
|
||||
{
|
||||
return (getUniqueKeyToPrimaryKeyMap("ignore", records).keySet());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Map<List<Serializable>, Serializable> getUniqueKeyToPrimaryKeyMap(String primaryKeyField, List<QRecord> records)
|
||||
{
|
||||
Map<List<Serializable>, Serializable> rs = new HashMap<>();
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
List<Serializable> uniqueKeyValues = getUniqueKeyValues(record);
|
||||
rs.put(uniqueKeyValues, record.getValue(primaryKeyField));
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<Serializable> getUniqueKeyValues(QRecord record)
|
||||
{
|
||||
List<Serializable> uniqueKeyValues = new ArrayList<>();
|
||||
for(String fieldName : cacheUniqueKey.getFieldNames())
|
||||
{
|
||||
uniqueKeyValues.add(record.getValue(fieldName));
|
||||
}
|
||||
return uniqueKeyValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** figure out if this was a request that we can cache records for -
|
||||
** e.g., if it's a request for unique-key EQUALS or IN
|
||||
** build up fields for the unique keys, the values, etc.
|
||||
*******************************************************************************/
|
||||
private void analyzeInput(QueryInput queryInput)
|
||||
{
|
||||
QTableMetaData table = queryInput.getTable();
|
||||
|
||||
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
|
||||
{
|
||||
cacheUseCaseMap.put(cacheUseCase.getType(), cacheUseCase);
|
||||
}
|
||||
|
||||
if(cacheUseCaseMap.containsKey(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY))
|
||||
{
|
||||
if(queryInput.getFilter() == null)
|
||||
{
|
||||
LOG.trace("Unable to cache: there is no filter");
|
||||
return;
|
||||
}
|
||||
|
||||
QQueryFilter filter = queryInput.getFilter();
|
||||
Set<String> queryFields = new HashSet<>();
|
||||
if(CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(filter.getCriteria()))
|
||||
{
|
||||
LOG.trace("Unable to cache: we have sub-filters and criteria");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!QQueryFilter.BooleanOperator.OR.equals(filter.getBooleanOperator()))
|
||||
{
|
||||
LOG.trace("Unable to cache: we have sub-filters but not an OR query");
|
||||
return;
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// look at sub-filters //
|
||||
/////////////////////////
|
||||
for(QQueryFilter subFilter : filter.getSubFilters())
|
||||
{
|
||||
Set<String> thisSubFilterFields = getQueryFieldsIfCacheableFilter(subFilter, false);
|
||||
if(thisSubFilterFields == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(queryFields.isEmpty())
|
||||
{
|
||||
queryFields.addAll(thisSubFilterFields);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!queryFields.equals(thisSubFilterFields))
|
||||
{
|
||||
LOG.trace("Unable to cache: sub-filters have different sets of fields");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(doQueryFieldsMatchAUniqueKey(table, queryFields))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.trace("Unable to cache: we have sub-filters that do match a unique key");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////
|
||||
// look at the criteria in the query: //
|
||||
// - build a set of field names //
|
||||
// - fail upon unsupported operators //
|
||||
// - collect the values in the criteria //
|
||||
//////////////////////////////////////////
|
||||
queryFields = getQueryFieldsIfCacheableFilter(filter, true);
|
||||
if(queryFields == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(doQueryFieldsMatchAUniqueKey(table, queryFields))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.trace("Unable to cache: we have query fields that don't match a unique key: " + queryFields);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.trace("Unable to cache: No supported use case: " + cacheUseCaseMap.keySet());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean doQueryFieldsMatchAUniqueKey(QTableMetaData table, Set<String> queryFields)
|
||||
{
|
||||
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
|
||||
{
|
||||
if(queryFields.equals(new HashSet<>(uniqueKey.getFieldNames())))
|
||||
{
|
||||
this.cacheUniqueKey = uniqueKey;
|
||||
isQueryInputCacheable = true;
|
||||
activeCacheUseCase = cacheUseCaseMap.get(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<String> getQueryFieldsIfCacheableFilter(QQueryFilter filter, boolean allowOperatorIn)
|
||||
{
|
||||
Set<String> rs = new HashSet<>();
|
||||
for(QFilterCriteria criterion : filter.getCriteria())
|
||||
{
|
||||
boolean isEquals = criterion.getOperator().equals(QCriteriaOperator.EQUALS);
|
||||
boolean isIn = criterion.getOperator().equals(QCriteriaOperator.IN);
|
||||
|
||||
if(isEquals || (isIn && allowOperatorIn))
|
||||
{
|
||||
rs.add(criterion.getFieldName());
|
||||
this.uniqueKeyValues.addAll(criterion.getFieldName(), criterion.getValues());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.trace("Unable to cache: we have an unsupported criteria operator: " + criterion.getOperator());
|
||||
isQueryInputCacheable = false;
|
||||
return (null);
|
||||
}
|
||||
}
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QRecord> tryToGetFromCacheSource(QueryInput queryInput, Set<List<Serializable>> uniqueKeyValues) throws QException
|
||||
{
|
||||
List<QRecord> recordsFromSource = null;
|
||||
QTableMetaData table = queryInput.getTable();
|
||||
|
||||
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(activeCacheUseCase.getType()))
|
||||
{
|
||||
recordsFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(queryInput, uniqueKeyValues, table.getCacheOf().getSourceTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo!!
|
||||
throw (new NotImplementedException("Not-yet-implemented cache use case type: " + activeCacheUseCase.getType()));
|
||||
}
|
||||
|
||||
return (recordsFromSource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QRecord> getFromCachedSourceForUniqueKeyToUniqueKey(QueryInput cacheQueryInput, Set<List<Serializable>> uniqueKeyValues, String sourceTableName) throws QException
|
||||
{
|
||||
QTableMetaData sourceTable = QContext.getQInstance().getTable(sourceTableName);
|
||||
QBackendMetaData sourceBackend = QContext.getQInstance().getBackendForTable(sourceTableName);
|
||||
|
||||
if(sourceTable.isCapabilityEnabled(sourceBackend, Capability.TABLE_QUERY))
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// do a Query on the source table, by the unique key //
|
||||
///////////////////////////////////////////////////////
|
||||
QueryInput sourceQueryInput = new QueryInput();
|
||||
sourceQueryInput.setTableName(sourceTableName);
|
||||
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
sourceQueryInput.setFilter(filter);
|
||||
sourceQueryInput.setCommonParamsFrom(cacheQueryInput);
|
||||
|
||||
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
|
||||
{
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
filter.addSubFilter(subFilter);
|
||||
|
||||
for(int i = 0; i < cacheUniqueKey.getFieldNames().size(); i++)
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(cacheUniqueKey.getFieldNames().get(i), QCriteriaOperator.EQUALS, uniqueKeyValue.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
QueryOutput sourceQueryOutput = new QueryAction().execute(sourceQueryInput);
|
||||
return (sourceQueryOutput.getRecords());
|
||||
}
|
||||
else if(sourceTable.isCapabilityEnabled(sourceBackend, Capability.TABLE_GET))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if the table only supports GET, then do a GET for each unique key //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> outputRecords = new ArrayList<>();
|
||||
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
|
||||
{
|
||||
Map<String, Serializable> uniqueKey = new HashMap<>();
|
||||
for(int i = 0; i < cacheUniqueKey.getFieldNames().size(); i++)
|
||||
{
|
||||
uniqueKey.put(cacheUniqueKey.getFieldNames().get(i), uniqueKeyValue.get(i));
|
||||
}
|
||||
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(sourceTableName);
|
||||
getInput.setUniqueKey(uniqueKey);
|
||||
getInput.setCommonParamsFrom(cacheQueryInput);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
|
||||
if(getOutput.getRecord() != null)
|
||||
{
|
||||
outputRecords.add(getOutput.getRecord());
|
||||
}
|
||||
}
|
||||
|
||||
return (outputRecords);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Cache source table " + sourceTableName + " does not support Query or Get capability."));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,640 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
|
||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Singleton, which starts a thread, to store query stats into a table.
|
||||
**
|
||||
** Supports these systemProperties or ENV_VARS:
|
||||
** qqq.queryStatManager.enabled / QQQ_QUERY_STAT_MANAGER_ENABLED
|
||||
** qqq.queryStatManager.minMillisToStore / QQQ_QUERY_STAT_MANAGER_MIN_MILLIS_TO_STORE
|
||||
** qqq.queryStatManager.jobPeriodSeconds / QQQ_QUERY_STAT_MANAGER_JOB_PERIOD_SECONDS
|
||||
** qqq.queryStatManager.jobInitialDelay / QQQ_QUERY_STAT_MANAGER_JOB_INITIAL_DELAY
|
||||
*******************************************************************************/
|
||||
public class QueryStatManager
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QueryStatManager.class);
|
||||
|
||||
private static QueryStatManager queryStatManager = null;
|
||||
|
||||
// todo - support multiple qInstances?
|
||||
private QInstance qInstance;
|
||||
private Supplier<QSession> sessionSupplier;
|
||||
|
||||
private boolean active = false;
|
||||
private List<QueryStat> queryStats = new ArrayList<>();
|
||||
|
||||
private ScheduledExecutorService executorService;
|
||||
|
||||
private int jobPeriodSeconds = 60;
|
||||
private int jobInitialDelay = 60;
|
||||
private int minMillisToStore = 0;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Singleton constructor
|
||||
*******************************************************************************/
|
||||
private QueryStatManager()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Singleton accessor
|
||||
*******************************************************************************/
|
||||
public static QueryStatManager getInstance()
|
||||
{
|
||||
if(queryStatManager == null)
|
||||
{
|
||||
queryStatManager = new QueryStatManager();
|
||||
|
||||
QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter();
|
||||
|
||||
Integer propertyMinMillisToStore = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.minMillisToStore", "QQQ_QUERY_STAT_MANAGER_MIN_MILLIS_TO_STORE", null);
|
||||
if(propertyMinMillisToStore != null)
|
||||
{
|
||||
queryStatManager.setMinMillisToStore(propertyMinMillisToStore);
|
||||
}
|
||||
|
||||
Integer propertyJobPeriodSeconds = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.jobPeriodSeconds", "QQQ_QUERY_STAT_MANAGER_JOB_PERIOD_SECONDS", null);
|
||||
if(propertyJobPeriodSeconds != null)
|
||||
{
|
||||
queryStatManager.setJobPeriodSeconds(propertyJobPeriodSeconds);
|
||||
}
|
||||
|
||||
Integer propertyJobInitialDelay = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.jobInitialDelay", "QQQ_QUERY_STAT_MANAGER_JOB_INITIAL_DELAY", null);
|
||||
if(propertyJobInitialDelay != null)
|
||||
{
|
||||
queryStatManager.setJobInitialDelay(propertyJobInitialDelay);
|
||||
}
|
||||
|
||||
}
|
||||
return (queryStatManager);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QueryStat newQueryStat(QBackendMetaData backend, QTableMetaData table, QQueryFilter filter)
|
||||
{
|
||||
QueryStat queryStat = null;
|
||||
|
||||
if(table.isCapabilityEnabled(backend, Capability.QUERY_STATS))
|
||||
{
|
||||
queryStat = new QueryStat();
|
||||
queryStat.setTableName(table.getName());
|
||||
queryStat.setQueryFilter(Objects.requireNonNullElse(filter, new QQueryFilter()));
|
||||
queryStat.setStartTimestamp(Instant.now());
|
||||
}
|
||||
|
||||
return (queryStat);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void start(QInstance qInstance, Supplier<QSession> sessionSupplier)
|
||||
{
|
||||
if(!isEnabled())
|
||||
{
|
||||
LOG.info("Not starting QueryStatManager per settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Starting QueryStatManager");
|
||||
|
||||
this.qInstance = qInstance;
|
||||
this.sessionSupplier = sessionSupplier;
|
||||
|
||||
active = true;
|
||||
queryStats = new ArrayList<>();
|
||||
|
||||
executorService = Executors.newSingleThreadScheduledExecutor();
|
||||
executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), jobInitialDelay, jobPeriodSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static boolean isEnabled()
|
||||
{
|
||||
return new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.queryStatManager.enabled", "QQQ_QUERY_STAT_MANAGER_ENABLED", true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void stop()
|
||||
{
|
||||
active = false;
|
||||
queryStats.clear();
|
||||
|
||||
if(executorService != null)
|
||||
{
|
||||
executorService.shutdown();
|
||||
executorService = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void add(QueryStat queryStat)
|
||||
{
|
||||
if(queryStat == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(active)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set fields that we need to capture now (rather than when the thread to store runs) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(queryStat.getFirstResultTimestamp() == null)
|
||||
{
|
||||
queryStat.setFirstResultTimestamp(Instant.now());
|
||||
}
|
||||
|
||||
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
|
||||
{
|
||||
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
|
||||
queryStat.setFirstResultMillis((int) millis);
|
||||
}
|
||||
|
||||
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// discard this record if it's under the min millis setting //
|
||||
//////////////////////////////////////////////////////////////
|
||||
return;
|
||||
}
|
||||
|
||||
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
|
||||
{
|
||||
queryStat.setSessionId(QContext.getQSession().getUuid());
|
||||
}
|
||||
|
||||
if(queryStat.getAction() == null)
|
||||
{
|
||||
if(!QContext.getActionStack().isEmpty())
|
||||
{
|
||||
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
|
||||
}
|
||||
else
|
||||
{
|
||||
boolean expected = false;
|
||||
Exception e = new Exception("Unexpected empty action stack");
|
||||
for(StackTraceElement stackTraceElement : e.getStackTrace())
|
||||
{
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains(QueryStatManagerInsertJob.class.getName()))
|
||||
{
|
||||
expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!expected)
|
||||
{
|
||||
LOG.debug(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
queryStats.add(queryStat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QueryStat> getListAndReset()
|
||||
{
|
||||
if(queryStats.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
synchronized(this)
|
||||
{
|
||||
List<QueryStat> returnList = queryStats;
|
||||
queryStats = new ArrayList<>();
|
||||
return (returnList);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** force stats to be stored right now (rather than letting the scheduled job do it)
|
||||
*******************************************************************************/
|
||||
public void storeStatsNow()
|
||||
{
|
||||
new QueryStatManagerInsertJob().run();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Runnable that gets scheduled to periodically reset and store the list of collected stats
|
||||
*******************************************************************************/
|
||||
private static class QueryStatManagerInsertJob implements Runnable
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QueryStatManagerInsertJob.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
QContext.init(getInstance().qInstance, getInstance().sessionSupplier.get());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// every time we re-run, check if we've been turned off - if so, stop the service. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!isEnabled())
|
||||
{
|
||||
LOG.info("Stopping QueryStatManager.");
|
||||
getInstance().stop();
|
||||
return;
|
||||
}
|
||||
|
||||
List<QueryStat> list = getInstance().getListAndReset();
|
||||
|
||||
LOG.info(logPair("queryStatListSize", list.size()));
|
||||
|
||||
if(list.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
// prime the entities for storing //
|
||||
////////////////////////////////////
|
||||
List<QRecord> queryStatQRecordsToInsert = new ArrayList<>();
|
||||
for(QueryStat queryStat : list)
|
||||
{
|
||||
try
|
||||
{
|
||||
//////////////////////
|
||||
// set the table id //
|
||||
//////////////////////
|
||||
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
|
||||
queryStat.setQqqTableId(qqqTableId);
|
||||
|
||||
//////////////////////////////
|
||||
// build join-table records //
|
||||
//////////////////////////////
|
||||
if(CollectionUtils.nullSafeHasContents(queryStat.getJoinTableNames()))
|
||||
{
|
||||
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
|
||||
for(String joinTableName : queryStat.getJoinTableNames())
|
||||
{
|
||||
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
|
||||
}
|
||||
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
|
||||
}
|
||||
|
||||
////////////////////////////
|
||||
// build criteria records //
|
||||
////////////////////////////
|
||||
if(queryStat.getQueryFilter() != null && queryStat.getQueryFilter().hasAnyCriteria())
|
||||
{
|
||||
List<QueryStatCriteriaField> queryStatCriteriaFieldList = new ArrayList<>();
|
||||
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, queryStat.getQueryFilter());
|
||||
queryStat.setQueryStatCriteriaFieldList(queryStatCriteriaFieldList);
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(queryStat.getQueryFilter().getOrderBys()))
|
||||
{
|
||||
List<QueryStatOrderByField> queryStatOrderByFieldList = new ArrayList<>();
|
||||
processOrderByFromFilter(qqqTableId, queryStatOrderByFieldList, queryStat.getQueryFilter());
|
||||
queryStat.setQueryStatOrderByFieldList(queryStatOrderByFieldList);
|
||||
}
|
||||
|
||||
queryStatQRecordsToInsert.add(queryStat.toQRecord());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////////////////
|
||||
// skip this record //
|
||||
//////////////////////
|
||||
LOG.warn("Error priming a query stat for storing", e);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(QueryStat.TABLE_NAME);
|
||||
insertInput.setRecords(queryStatQRecordsToInsert);
|
||||
new InsertAction().execute(insertInput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error inserting query stats", e);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error storing query stats", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processCriteriaFromFilter(Integer qqqTableId, List<QueryStatCriteriaField> queryStatCriteriaFieldList, QQueryFilter queryFilter) throws QException
|
||||
{
|
||||
for(QFilterCriteria criteria : CollectionUtils.nonNullList(queryFilter.getCriteria()))
|
||||
{
|
||||
String fieldName = criteria.getFieldName();
|
||||
QueryStatCriteriaField queryStatCriteriaField = new QueryStatCriteriaField();
|
||||
queryStatCriteriaField.setOperator(String.valueOf(criteria.getOperator()));
|
||||
|
||||
if(criteria.getValues() != null)
|
||||
{
|
||||
queryStatCriteriaField.setValues(StringUtils.join(",", criteria.getValues()));
|
||||
}
|
||||
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String[] parts = fieldName.split("\\.");
|
||||
if(parts.length > 1)
|
||||
{
|
||||
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
|
||||
queryStatCriteriaField.setName(parts[1]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
queryStatCriteriaField.setQqqTableId(qqqTableId);
|
||||
queryStatCriteriaField.setName(fieldName);
|
||||
}
|
||||
|
||||
queryStatCriteriaFieldList.add(queryStatCriteriaField);
|
||||
}
|
||||
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
|
||||
{
|
||||
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, subFilter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processOrderByFromFilter(Integer qqqTableId, List<QueryStatOrderByField> queryStatOrderByFieldList, QQueryFilter queryFilter) throws QException
|
||||
{
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
|
||||
{
|
||||
String fieldName = orderBy.getFieldName();
|
||||
QueryStatOrderByField queryStatOrderByField = new QueryStatOrderByField();
|
||||
|
||||
if(fieldName != null)
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String[] parts = fieldName.split("\\.");
|
||||
if(parts.length > 1)
|
||||
{
|
||||
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
|
||||
queryStatOrderByField.setName(parts[1]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
queryStatOrderByField.setQqqTableId(qqqTableId);
|
||||
queryStatOrderByField.setName(fieldName);
|
||||
}
|
||||
|
||||
queryStatOrderByFieldList.add(queryStatOrderByField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer getQQQTableId(String tableName) throws QException
|
||||
{
|
||||
/////////////////////////////
|
||||
// look in the cache table //
|
||||
/////////////////////////////
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
|
||||
getInput.setUniqueKey(MapBuilder.of("name", tableName));
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
|
||||
////////////////////////
|
||||
// upon cache miss... //
|
||||
////////////////////////
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// insert the record (into the table, not the cache) //
|
||||
///////////////////////////////////////////////////////
|
||||
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(QQQTable.TABLE_NAME);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
///////////////////////////////////
|
||||
// repeat the get from the cache //
|
||||
///////////////////////////////////
|
||||
getOutput = new GetAction().execute(getInput);
|
||||
}
|
||||
|
||||
return getOutput.getRecord().getValueInteger("id");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for jobPeriodSeconds
|
||||
*******************************************************************************/
|
||||
public int getJobPeriodSeconds()
|
||||
{
|
||||
return (this.jobPeriodSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for jobPeriodSeconds
|
||||
*******************************************************************************/
|
||||
public void setJobPeriodSeconds(int jobPeriodSeconds)
|
||||
{
|
||||
this.jobPeriodSeconds = jobPeriodSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for jobPeriodSeconds
|
||||
*******************************************************************************/
|
||||
public QueryStatManager withJobPeriodSeconds(int jobPeriodSeconds)
|
||||
{
|
||||
this.jobPeriodSeconds = jobPeriodSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for jobInitialDelay
|
||||
*******************************************************************************/
|
||||
public int getJobInitialDelay()
|
||||
{
|
||||
return (this.jobInitialDelay);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for jobInitialDelay
|
||||
*******************************************************************************/
|
||||
public void setJobInitialDelay(int jobInitialDelay)
|
||||
{
|
||||
this.jobInitialDelay = jobInitialDelay;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for jobInitialDelay
|
||||
*******************************************************************************/
|
||||
public QueryStatManager withJobInitialDelay(int jobInitialDelay)
|
||||
{
|
||||
this.jobInitialDelay = jobInitialDelay;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for minMillisToStore
|
||||
*******************************************************************************/
|
||||
public int getMinMillisToStore()
|
||||
{
|
||||
return (this.minMillisToStore);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for minMillisToStore
|
||||
*******************************************************************************/
|
||||
public void setMinMillisToStore(int minMillisToStore)
|
||||
{
|
||||
this.minMillisToStore = minMillisToStore;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for minMillisToStore
|
||||
*******************************************************************************/
|
||||
public QueryStatManager withMinMillisToStore(int minMillisToStore)
|
||||
{
|
||||
this.minMillisToStore = minMillisToStore;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -44,6 +44,7 @@ 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.security.QSecurityKeyType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -68,6 +69,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
{
|
||||
INSERT,
|
||||
UPDATE,
|
||||
DELETE,
|
||||
SELECT
|
||||
}
|
||||
|
||||
@ -78,7 +80,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
*******************************************************************************/
|
||||
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
|
||||
{
|
||||
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table);
|
||||
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
|
||||
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
|
||||
{
|
||||
return;
|
||||
@ -98,11 +100,12 @@ public class ValidateRecordSecurityLockHelper
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()))
|
||||
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
// if not updating the security field, then no error can come from it! //
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
|
||||
// So if we're not updating the security field, then no error can come from it! //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -244,11 +247,18 @@ public class ValidateRecordSecurityLockHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table)
|
||||
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
|
||||
{
|
||||
List<RecordSecurityLock> recordSecurityLocks = table.getRecordSecurityLocks();
|
||||
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
|
||||
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
|
||||
|
||||
recordSecurityLocks = switch(action)
|
||||
{
|
||||
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
|
||||
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
|
||||
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
|
||||
};
|
||||
|
||||
////////////////////////////////////////
|
||||
// if there are no locks, just return //
|
||||
////////////////////////////////////////
|
||||
@ -281,7 +291,7 @@ public class ValidateRecordSecurityLockHelper
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
|
||||
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
|
||||
{
|
||||
if(recordSecurityValue == null)
|
||||
{
|
||||
|
@ -26,8 +26,8 @@ import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfInput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfOutput;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
|
@ -28,8 +28,8 @@ import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.velocity.VelocityContext;
|
||||
@ -62,7 +62,15 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
|
||||
if(TemplateType.VELOCITY.equals(input.getTemplateType()))
|
||||
{
|
||||
Velocity.init();
|
||||
Context context = new VelocityContext(input.getContext());
|
||||
Context context = new VelocityContext();
|
||||
|
||||
if(input.getContext() != null)
|
||||
{
|
||||
for(Map.Entry<String, ?> entry : input.getContext().entrySet())
|
||||
{
|
||||
context.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
setupEventHandlers(context);
|
||||
|
||||
|
@ -136,7 +136,7 @@ public class QValueFormatter
|
||||
{
|
||||
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
|
||||
}
|
||||
else if(e.getMessage().equals("d != java.math.BigDecimal"))
|
||||
else if(e.getMessage().equals("d != java.math.BigDecimal") || e.getMessage().equals("d != java.lang.String"))
|
||||
{
|
||||
return formatValue(displayFormat, ValueUtils.getValueAsInteger(value));
|
||||
}
|
||||
|
@ -244,8 +244,6 @@ public class SearchPossibleValueSourceAction
|
||||
}
|
||||
}
|
||||
|
||||
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
|
||||
|
||||
// todo - skip & limit as params
|
||||
queryFilter.setLimit(250);
|
||||
|
||||
@ -257,6 +255,9 @@ public class SearchPossibleValueSourceAction
|
||||
input.getDefaultQueryFilter().addSubFilter(queryFilter);
|
||||
queryFilter = input.getDefaultQueryFilter();
|
||||
}
|
||||
|
||||
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
|
||||
|
||||
queryInput.setFilter(queryFilter);
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
@ -84,7 +84,7 @@ public class QContext
|
||||
actionStackThreadLocal.get().add(actionInput);
|
||||
}
|
||||
|
||||
if(!qInstance.getHasBeenValidated())
|
||||
if(qInstance != null && !qInstance.getHasBeenValidated())
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -22,12 +22,19 @@
|
||||
package com.kingsrook.qqq.backend.core.exceptions;
|
||||
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Base class for checked exceptions thrown in qqq.
|
||||
*
|
||||
*******************************************************************************/
|
||||
public class QException extends Exception
|
||||
{
|
||||
private boolean hasLoggedWarning;
|
||||
private boolean hasLoggedError;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message
|
||||
@ -59,4 +66,102 @@ public class QException extends Exception
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for hasLoggedWarning
|
||||
*******************************************************************************/
|
||||
public boolean getHasLoggedWarning()
|
||||
{
|
||||
return (this.hasLoggedWarning);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for hasLoggedWarning
|
||||
*******************************************************************************/
|
||||
public void setHasLoggedWarning(boolean hasLoggedWarning)
|
||||
{
|
||||
this.hasLoggedWarning = hasLoggedWarning;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for hasLoggedWarning
|
||||
*******************************************************************************/
|
||||
public QException withHasLoggedWarning(boolean hasLoggedWarning)
|
||||
{
|
||||
this.hasLoggedWarning = hasLoggedWarning;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for hasLoggedError
|
||||
*******************************************************************************/
|
||||
public boolean getHasLoggedError()
|
||||
{
|
||||
return (this.hasLoggedError);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for hasLoggedError
|
||||
*******************************************************************************/
|
||||
public void setHasLoggedError(boolean hasLoggedError)
|
||||
{
|
||||
this.hasLoggedError = hasLoggedError;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for hasLoggedError
|
||||
*******************************************************************************/
|
||||
public QException withHasLoggedError(boolean hasLoggedError)
|
||||
{
|
||||
this.hasLoggedError = hasLoggedError;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** helper function for getting if level logged
|
||||
*******************************************************************************/
|
||||
public boolean hasLoggedLevel(Level level)
|
||||
{
|
||||
if(Level.WARN.equals(level))
|
||||
{
|
||||
return (hasLoggedWarning);
|
||||
}
|
||||
if(Level.ERROR.equals(level))
|
||||
{
|
||||
return (hasLoggedError);
|
||||
}
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** helper function for setting if level logged
|
||||
*******************************************************************************/
|
||||
public void setHasLoggedLevel(Level level)
|
||||
{
|
||||
if(Level.WARN.equals(level))
|
||||
{
|
||||
setHasLoggedWarning(true);
|
||||
}
|
||||
if(Level.ERROR.equals(level))
|
||||
{
|
||||
setHasLoggedError(true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@ -57,12 +58,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
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.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteLoadStep;
|
||||
@ -96,6 +98,13 @@ public class QInstanceEnricher
|
||||
//////////////////////////////////////////////////////////
|
||||
private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// let an instance define mappings to be applied during name-to-label enrichments, //
|
||||
// e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" should always be "TLA") //
|
||||
// or to expand abbreviations in code (e.g., "Addr" should always be "Address" //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private static final Map<String, String> labelMappings = new LinkedHashMap<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -261,9 +270,9 @@ public class QInstanceEnricher
|
||||
{
|
||||
table.getFields().values().forEach(this::enrichField);
|
||||
|
||||
for(QMiddlewareTableMetaData middlewareTableMetaData : CollectionUtils.nonNullMap(table.getMiddlewareMetaData()).values())
|
||||
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
|
||||
{
|
||||
middlewareTableMetaData.enrich(table);
|
||||
supplementalTableMetaData.enrich(qInstance, table);
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,6 +376,11 @@ public class QInstanceEnricher
|
||||
process.getStepList().forEach(this::enrichStep);
|
||||
}
|
||||
|
||||
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
|
||||
{
|
||||
supplementalProcessMetaData.enrich(this, process);
|
||||
}
|
||||
|
||||
enrichPermissionRules(process);
|
||||
}
|
||||
|
||||
@ -641,7 +655,17 @@ public class QInstanceEnricher
|
||||
////////////////////////////////////////////////////////////////
|
||||
.replaceAll("([0-9])([A-Za-z])", "$1 $2");
|
||||
|
||||
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix);
|
||||
String label = name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// apply any label mappings - e.g., to force app-specific acronyms/initialisms to all-caps //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(Map.Entry<String, String> entry : labelMappings.entrySet())
|
||||
{
|
||||
label = label.replaceAll(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
return (label);
|
||||
}
|
||||
|
||||
|
||||
@ -1105,4 +1129,35 @@ public class QInstanceEnricher
|
||||
{
|
||||
return (this.joinGraph);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void addLabelMapping(String from, String to)
|
||||
{
|
||||
labelMappings.put(from, to);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void removeLabelMapping(String from)
|
||||
{
|
||||
labelMappings.remove(from);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void clearLabelMappings()
|
||||
{
|
||||
labelMappings.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
|
||||
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.fields.AdornmentType;
|
||||
@ -63,6 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||
@ -158,7 +159,7 @@ public class QInstanceValidator
|
||||
validateQueuesAndProviders(qInstance);
|
||||
validateJoins(qInstance);
|
||||
validateSecurityKeyTypes(qInstance);
|
||||
validateMiddlewareMetaData(qInstance);
|
||||
validateSupplementalMetaData(qInstance);
|
||||
|
||||
validateUniqueTopLevelNames(qInstance);
|
||||
}
|
||||
@ -182,11 +183,11 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateMiddlewareMetaData(QInstance qInstance)
|
||||
private void validateSupplementalMetaData(QInstance qInstance)
|
||||
{
|
||||
for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values())
|
||||
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getSupplementalMetaData()).values())
|
||||
{
|
||||
middlewareInstanceMetaData.validate(qInstance, this);
|
||||
supplementalInstanceMetaData.validate(qInstance, this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -437,10 +438,13 @@ public class QInstanceValidator
|
||||
for(QFieldSection section : table.getSections())
|
||||
{
|
||||
validateTableSection(qInstance, table, section, fieldNamesInSections);
|
||||
if(section.getTier().equals(Tier.T1))
|
||||
if(assertCondition(section.getTier() != null, "Table " + tableName + " " + section.getName() + " is missing its tier"))
|
||||
{
|
||||
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
|
||||
tier1Section = section;
|
||||
if(section.getTier().equals(Tier.T1))
|
||||
{
|
||||
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
|
||||
tier1Section = section;
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName());
|
||||
@ -572,6 +576,11 @@ public class QInstanceValidator
|
||||
RECORD_SECURITY_LOCKS_LOOP:
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
|
||||
{
|
||||
@ -580,6 +589,8 @@ public class QInstanceValidator
|
||||
|
||||
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
|
||||
|
||||
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
|
||||
|
||||
boolean hasAnyBadJoins = false;
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
@ -1093,13 +1104,39 @@ public class QInstanceValidator
|
||||
boolean hasFields = CollectionUtils.nullSafeHasContents(section.getFieldNames());
|
||||
boolean hasWidget = StringUtils.hasContent(section.getWidgetName());
|
||||
|
||||
if(assertCondition(hasFields || hasWidget, "Table " + table.getName() + " section " + section.getName() + " does not have any fields or a widget."))
|
||||
String sectionPrefix = "Table " + table.getName() + " section " + section.getName() + " ";
|
||||
if(assertCondition(hasFields || hasWidget, sectionPrefix + "does not have any fields or a widget."))
|
||||
{
|
||||
if(table.getFields() != null && hasFields)
|
||||
{
|
||||
for(String fieldName : section.getFieldNames())
|
||||
{
|
||||
assertCondition(table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table.");
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - this was originally written as an assertion: //
|
||||
// if(assertCondition(qInstance.getTable(otherTableName) != null, sectionPrefix + "join-field " + fieldName + ", which is referencing an unrecognized table name [" + otherTableName + "]")) //
|
||||
// but... then a field name with dots gives us a bad time here, so... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldName.contains(".") && qInstance.getTable(fieldName.split("\\.")[0]) != null)
|
||||
{
|
||||
String[] parts = fieldName.split("\\.");
|
||||
String otherTableName = parts[0];
|
||||
String foreignFieldName = parts[1];
|
||||
|
||||
if(assertCondition(qInstance.getTable(otherTableName) != null, sectionPrefix + "join-field " + fieldName + ", which is referencing an unrecognized table name [" + otherTableName + "]"))
|
||||
{
|
||||
List<ExposedJoin> matchedExposedJoins = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> otherTableName.equals(ej.getJoinTable())).toList();
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(matchedExposedJoins), sectionPrefix + "join-field " + fieldName + ", referencing table [" + otherTableName + "] which is not an exposed join on this table."))
|
||||
{
|
||||
assertCondition(!matchedExposedJoins.get(0).getIsMany(qInstance), sectionPrefix + "join-field " + fieldName + " references an is-many join, which is not supported.");
|
||||
}
|
||||
assertCondition(qInstance.getTable(otherTableName).getFields().containsKey(foreignFieldName), sectionPrefix + "join-field " + fieldName + " specifies a fieldName [" + foreignFieldName + "] which does not exist in that table [" + otherTableName + "].");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assertCondition(table.getFields().containsKey(fieldName), sectionPrefix + "specifies fieldName " + fieldName + ", which is not a field on this table.");
|
||||
}
|
||||
|
||||
assertCondition(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections.");
|
||||
|
||||
fieldNamesInSections.add(fieldName);
|
||||
@ -1107,7 +1144,7 @@ public class QInstanceValidator
|
||||
}
|
||||
else if(hasWidget)
|
||||
{
|
||||
assertCondition(qInstance.getWidget(section.getWidgetName()) != null, "Table " + table.getName() + " section " + section.getName() + " specifies widget " + section.getWidgetName() + ", which is not a widget in this instance.");
|
||||
assertCondition(qInstance.getWidget(section.getWidgetName()) != null, sectionPrefix + "specifies widget " + section.getWidgetName() + ", which is not a widget in this instance.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1218,14 +1255,18 @@ public class QInstanceValidator
|
||||
QScheduleMetaData schedule = process.getSchedule();
|
||||
assertCondition(schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null, "Either repeat millis or repeat seconds must be set on schedule in process " + processName);
|
||||
|
||||
if(schedule.getBackendVariant() != null)
|
||||
if(schedule.getVariantBackend() != null)
|
||||
{
|
||||
assertCondition(schedule.getVariantRunStrategy() != null, "A variant strategy was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
|
||||
assertCondition(schedule.getVariantTableName() != null, "A variant table name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
|
||||
assertCondition(schedule.getVariantFieldName() != null, "A variant field name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
|
||||
assertCondition(qInstance.getBackend(schedule.getVariantBackend()) != null, "A variant backend was not found for " + schedule.getVariantBackend());
|
||||
assertCondition(schedule.getVariantRunStrategy() != null, "A variant run strategy was not set for " + schedule.getVariantBackend() + " on schedule in process " + processName);
|
||||
}
|
||||
}
|
||||
|
||||
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
|
||||
{
|
||||
supplementalProcessMetaData.validate(qInstance, process, this);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1703,7 +1744,7 @@ public class QInstanceValidator
|
||||
** But if it throws, add the provided message to the list of errors (and return false,
|
||||
** e.g., in case you need to stop evaluating rules to avoid exceptions).
|
||||
*******************************************************************************/
|
||||
private boolean assertNoException(UnsafeLambda unsafeLambda, String message)
|
||||
public boolean assertNoException(UnsafeLambda unsafeLambda, String message)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -1736,7 +1777,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void warn(String message)
|
||||
public void warn(String message)
|
||||
{
|
||||
if(printWarnings)
|
||||
{
|
||||
|
@ -30,6 +30,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import io.github.cdimascio.dotenv.Dotenv;
|
||||
import io.github.cdimascio.dotenv.DotenvEntry;
|
||||
@ -266,4 +267,111 @@ public class QMetaDataVariableInterpreter
|
||||
|
||||
valueMaps.put(name, values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** First look for a boolean ("true" or "false") in the specified system property -
|
||||
** Next look for a boolean in the specified env var name -
|
||||
** Finally return the default.
|
||||
*******************************************************************************/
|
||||
public boolean getBooleanFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, boolean defaultIfNotSet)
|
||||
{
|
||||
String propertyValue = System.getProperty(systemPropertyName);
|
||||
if(StringUtils.hasContent(propertyValue))
|
||||
{
|
||||
if("false".equalsIgnoreCase(propertyValue))
|
||||
{
|
||||
LOG.info("Read system property [" + systemPropertyName + "] as boolean false.");
|
||||
return (false);
|
||||
}
|
||||
else if("true".equalsIgnoreCase(propertyValue))
|
||||
{
|
||||
LOG.info("Read system property [" + systemPropertyName + "] as boolean true.");
|
||||
return (true);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unrecognized boolean value [" + propertyValue + "] for system property [" + systemPropertyName + "].");
|
||||
}
|
||||
}
|
||||
|
||||
String envValue = interpret("${env." + environmentVariableName + "}");
|
||||
if(StringUtils.hasContent(envValue))
|
||||
{
|
||||
if("false".equalsIgnoreCase(envValue))
|
||||
{
|
||||
LOG.info("Read env var [" + environmentVariableName + "] as boolean false.");
|
||||
return (false);
|
||||
}
|
||||
else if("true".equalsIgnoreCase(envValue))
|
||||
{
|
||||
LOG.info("Read env var [" + environmentVariableName + "] as boolean true.");
|
||||
return (true);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unrecognized boolean value [" + envValue + "] for env var [" + environmentVariableName + "].");
|
||||
}
|
||||
}
|
||||
|
||||
return defaultIfNotSet;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** First look for an Integer in the specified system property -
|
||||
** Next look for an Integer in the specified env var name -
|
||||
** Finally return the default (null allowed as default!)
|
||||
*******************************************************************************/
|
||||
public Integer getIntegerFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, Integer defaultIfNotSet)
|
||||
{
|
||||
String propertyValue = System.getProperty(systemPropertyName);
|
||||
if(StringUtils.hasContent(propertyValue))
|
||||
{
|
||||
if(canParseAsInteger(propertyValue))
|
||||
{
|
||||
LOG.info("Read system property [" + systemPropertyName + "] as integer " + propertyValue);
|
||||
return (Integer.parseInt(propertyValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unrecognized integer value [" + propertyValue + "] for system property [" + systemPropertyName + "].");
|
||||
}
|
||||
}
|
||||
|
||||
String envValue = interpret("${env." + environmentVariableName + "}");
|
||||
if(StringUtils.hasContent(envValue))
|
||||
{
|
||||
if(canParseAsInteger(envValue))
|
||||
{
|
||||
LOG.info("Read env var [" + environmentVariableName + "] as integer " + environmentVariableName);
|
||||
return (Integer.parseInt(envValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unrecognized integer value [" + envValue + "] for env var [" + environmentVariableName + "].");
|
||||
}
|
||||
}
|
||||
|
||||
return defaultIfNotSet;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** we'd use NumberUtils.isDigits, but that doesn't allow negatives, or
|
||||
** numberUtils.isParseable, but that allows decimals, so...
|
||||
*******************************************************************************/
|
||||
private boolean canParseAsInteger(String value)
|
||||
{
|
||||
if(value == null)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
return (value.matches("^-?[0-9]+$"));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,10 +29,12 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@ -392,7 +394,7 @@ public class QLogger
|
||||
*******************************************************************************/
|
||||
public void warn(String message, Throwable t)
|
||||
{
|
||||
logger.warn(makeJsonString(message, t));
|
||||
logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
@ -402,7 +404,7 @@ public class QLogger
|
||||
*******************************************************************************/
|
||||
public void warn(String message, Throwable t, LogPair... logPairs)
|
||||
{
|
||||
logger.warn(makeJsonString(message, t, logPairs));
|
||||
logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t, logPairs));
|
||||
}
|
||||
|
||||
|
||||
@ -412,7 +414,7 @@ public class QLogger
|
||||
*******************************************************************************/
|
||||
public void warn(Throwable t)
|
||||
{
|
||||
logger.warn(makeJsonString(null, t));
|
||||
logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
@ -452,7 +454,7 @@ public class QLogger
|
||||
*******************************************************************************/
|
||||
public void error(String message, Throwable t)
|
||||
{
|
||||
logger.error(makeJsonString(message, t));
|
||||
logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
@ -462,7 +464,7 @@ public class QLogger
|
||||
*******************************************************************************/
|
||||
public void error(String message, Throwable t, LogPair... logPairs)
|
||||
{
|
||||
logger.error(makeJsonString(message, t, logPairs));
|
||||
logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t, logPairs));
|
||||
}
|
||||
|
||||
|
||||
@ -472,7 +474,7 @@ public class QLogger
|
||||
*******************************************************************************/
|
||||
public void error(Throwable t)
|
||||
{
|
||||
logger.error(makeJsonString(null, t));
|
||||
logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
@ -532,7 +534,7 @@ public class QLogger
|
||||
|
||||
if(t != null)
|
||||
{
|
||||
logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t))));
|
||||
logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(t))));
|
||||
}
|
||||
|
||||
return (LogUtils.jsonLog(logPairList));
|
||||
@ -582,4 +584,40 @@ public class QLogger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected Level determineIfShouldDowngrade(Throwable t, Level level)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// look for QExceptions in the chain, if none found, return the log level passed in //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QException> exceptionList = ExceptionUtils.getClassListFromRootChain(t, QException.class);
|
||||
if(CollectionUtils.nullSafeIsEmpty(exceptionList))
|
||||
{
|
||||
return (level);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// check if any QException in this chain to see if it has already //
|
||||
// logged this level, if so, downgrade to INFO //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
for(QException qException : exceptionList)
|
||||
{
|
||||
if(qException.hasLoggedLevel(level))
|
||||
{
|
||||
log(Level.DEBUG, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t);
|
||||
return (Level.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if it has not logged at this level, set that it has in QException, and return passed in level //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
exceptionList.get(0).setHasLoggedLevel(level);
|
||||
return (level);
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,16 @@ public class AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getActionIdentity()
|
||||
{
|
||||
return (getClass().getSimpleName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** performance instance validation (if not previously done).
|
||||
* // todo - verify this is happening (e.g., when context is set i guess)
|
||||
@ -145,14 +155,4 @@ public class AbstractActionInput
|
||||
this.asyncJobCallback = asyncJobCallback;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for instance
|
||||
*******************************************************************************/
|
||||
public AbstractActionInput withInstance(QInstance instance)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -46,6 +46,17 @@ public class AbstractTableActionInput extends AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getActionIdentity()
|
||||
{
|
||||
return (getClass().getSimpleName() + ":" + getTableName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -30,6 +30,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
@ -246,7 +247,7 @@ public class AuditSingleInput
|
||||
setAuditTableName(table.getName());
|
||||
|
||||
this.securityKeyValues = new HashMap<>();
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||
{
|
||||
this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName()));
|
||||
}
|
||||
|
@ -22,8 +22,10 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -76,6 +78,35 @@ public class ProcessSummaryFilterLink implements ProcessSummaryLineInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
public String getFullText()
|
||||
{
|
||||
StringBuilder rs = new StringBuilder();
|
||||
|
||||
if(StringUtils.hasContent(linkPreText))
|
||||
{
|
||||
rs.append(linkPreText).append(" ");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(linkText))
|
||||
{
|
||||
rs.append(linkText).append(" ");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(linkPostText))
|
||||
{
|
||||
rs.append(linkPostText).append(" ");
|
||||
}
|
||||
|
||||
rs.deleteCharAt(rs.length() - 1);
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -26,6 +26,7 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
@ -395,15 +396,19 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
|
||||
{
|
||||
if(count != null)
|
||||
{
|
||||
String baseMessage;
|
||||
if(count.equals(1))
|
||||
{
|
||||
setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage())
|
||||
+ (messageSuffix == null ? "" : messageSuffix));
|
||||
baseMessage = isPast ? getSingularPastMessage() : getSingularFutureMessage();
|
||||
}
|
||||
else
|
||||
{
|
||||
setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage())
|
||||
+ (messageSuffix == null ? "" : messageSuffix));
|
||||
baseMessage = isPast ? getPluralPastMessage() : getPluralFutureMessage();
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(baseMessage))
|
||||
{
|
||||
setMessage(baseMessage + ObjectUtils.requireConditionElse(messageSuffix, StringUtils::hasContent, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -64,6 +66,35 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@JsonIgnore
|
||||
public String getFullText()
|
||||
{
|
||||
StringBuilder rs = new StringBuilder();
|
||||
|
||||
if(StringUtils.hasContent(linkPreText))
|
||||
{
|
||||
rs.append(linkPreText).append(" ");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(linkText))
|
||||
{
|
||||
rs.append(linkText).append(" ");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(linkPostText))
|
||||
{
|
||||
rs.append(linkPostText).append(" ");
|
||||
}
|
||||
|
||||
rs.deleteCharAt(rs.length() - 1);
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
|
||||
@ -31,6 +35,7 @@ import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -71,6 +76,17 @@ public class RunProcessInput extends AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getActionIdentity()
|
||||
{
|
||||
return (getClass().getSimpleName() + ":" + getProcessName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** e.g., for steps after the first step in a process, seed the data in a run
|
||||
** function request from a process state.
|
||||
@ -183,6 +199,17 @@ public class RunProcessInput extends AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunProcessInput withValue(String fieldName, Serializable value)
|
||||
{
|
||||
this.processState.getValues().put(fieldName, value);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for values
|
||||
**
|
||||
@ -258,7 +285,7 @@ public class RunProcessInput extends AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public String getValueString(String fieldName)
|
||||
{
|
||||
return ((String) getValue(fieldName));
|
||||
return (ValueUtils.getValueAsString(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
@ -269,7 +296,67 @@ public class RunProcessInput extends AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public Integer getValueInteger(String fieldName)
|
||||
{
|
||||
return ((Integer) getValue(fieldName));
|
||||
return (ValueUtils.getValueAsInteger(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BigDecimal getValueBigDecimal(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsBigDecimal(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Boolean getValueBoolean(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsBoolean(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public LocalTime getValueLocalTime(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsLocalTime(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public LocalDate getValueLocalDate(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsLocalDate(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public byte[] getValueByteArray(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsByteArray(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Instant getValueInstant(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsInstant(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -122,6 +127,99 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for a single field's value
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Serializable getValue(String fieldName)
|
||||
{
|
||||
return (this.processState.getValues().get(fieldName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for a single field's value
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getValueString(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsString(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for a single field's value
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Integer getValueInteger(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsInteger(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BigDecimal getValueBigDecimal(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsBigDecimal(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Boolean getValueBoolean(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsBoolean(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public LocalTime getValueLocalTime(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsLocalTime(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public LocalDate getValueLocalDate(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsLocalDate(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public byte[] getValueByteArray(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsByteArray(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Instant getValueInstant(String fieldName)
|
||||
{
|
||||
return (ValueUtils.getValueAsInstant(getValue(fieldName)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for values
|
||||
**
|
||||
@ -133,6 +231,17 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunProcessOutput withValue(String fieldName, Serializable value)
|
||||
{
|
||||
this.processState.getValues().put(fieldName, value);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for values
|
||||
**
|
||||
|
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables;
|
||||
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Common getters & setters, shared by both QueryInput and GetInput.
|
||||
**
|
||||
** Original impetus for this class is the setCommonParamsFrom() method - for cases
|
||||
** where we need to change a Query to a Get, or vice-versa, and we want to copy over
|
||||
** all of those input params.
|
||||
*******************************************************************************/
|
||||
public interface QueryOrGetInputInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
** Set in THIS, the "common params" (e.g., common to both Query & Get inputs)
|
||||
** from the parameter SOURCE object.
|
||||
*******************************************************************************/
|
||||
default void setCommonParamsFrom(QueryOrGetInputInterface source)
|
||||
{
|
||||
this.setTransaction(source.getTransaction());
|
||||
this.setShouldTranslatePossibleValues(source.getShouldTranslatePossibleValues());
|
||||
this.setShouldGenerateDisplayValues(source.getShouldGenerateDisplayValues());
|
||||
this.setShouldFetchHeavyFields(source.getShouldFetchHeavyFields());
|
||||
this.setShouldOmitHiddenFields(source.getShouldOmitHiddenFields());
|
||||
this.setShouldMaskPasswords(source.getShouldMaskPasswords());
|
||||
this.setIncludeAssociations(source.getIncludeAssociations());
|
||||
this.setAssociationNamesToInclude(source.getAssociationNamesToInclude());
|
||||
this.setQueryJoins(source.getQueryJoins());
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for transaction
|
||||
*******************************************************************************/
|
||||
QBackendTransaction getTransaction();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for transaction
|
||||
*******************************************************************************/
|
||||
void setTransaction(QBackendTransaction transaction);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shouldTranslatePossibleValues
|
||||
*******************************************************************************/
|
||||
boolean getShouldTranslatePossibleValues();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shouldTranslatePossibleValues
|
||||
*******************************************************************************/
|
||||
void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shouldGenerateDisplayValues
|
||||
*******************************************************************************/
|
||||
boolean getShouldGenerateDisplayValues();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shouldGenerateDisplayValues
|
||||
*******************************************************************************/
|
||||
void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shouldFetchHeavyFields
|
||||
*******************************************************************************/
|
||||
boolean getShouldFetchHeavyFields();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shouldFetchHeavyFields
|
||||
*******************************************************************************/
|
||||
void setShouldFetchHeavyFields(boolean shouldFetchHeavyFields);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shouldOmitHiddenFields
|
||||
*******************************************************************************/
|
||||
boolean getShouldOmitHiddenFields();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shouldOmitHiddenFields
|
||||
*******************************************************************************/
|
||||
void setShouldOmitHiddenFields(boolean shouldOmitHiddenFields);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for shouldMaskPasswords
|
||||
*******************************************************************************/
|
||||
boolean getShouldMaskPasswords();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for shouldMaskPasswords
|
||||
*******************************************************************************/
|
||||
void setShouldMaskPasswords(boolean shouldMaskPasswords);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for includeAssociations
|
||||
*******************************************************************************/
|
||||
boolean getIncludeAssociations();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for includeAssociations
|
||||
*******************************************************************************/
|
||||
void setIncludeAssociations(boolean includeAssociations);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for associationNamesToInclude
|
||||
*******************************************************************************/
|
||||
Collection<String> getAssociationNamesToInclude();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for associationNamesToInclude
|
||||
*******************************************************************************/
|
||||
void setAssociationNamesToInclude(Collection<String> associationNamesToInclude);
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryJoins
|
||||
*******************************************************************************/
|
||||
List<QueryJoin> getQueryJoins();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryJoins
|
||||
**
|
||||
*******************************************************************************/
|
||||
void setQueryJoins(List<QueryJoin> queryJoins);
|
||||
|
||||
}
|
@ -40,6 +40,8 @@ public class AggregateInput extends AbstractTableActionInput
|
||||
private List<GroupBy> groupBys = new ArrayList<>();
|
||||
private Integer limit;
|
||||
|
||||
private Integer timeoutSeconds;
|
||||
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
|
||||
|
||||
@ -269,4 +271,35 @@ public class AggregateInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public Integer getTimeoutSeconds()
|
||||
{
|
||||
return (this.timeoutSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public void setTimeoutSeconds(Integer timeoutSeconds)
|
||||
{
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public AggregateInput withTimeoutSeconds(Integer timeoutSeconds)
|
||||
{
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -56,4 +56,14 @@ public enum AggregateOperator
|
||||
{
|
||||
return sqlPrefix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Aggregate of(String fieldName)
|
||||
{
|
||||
return (new Aggregate(fieldName, this));
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ public class CountInput extends AbstractTableActionInput
|
||||
{
|
||||
private QQueryFilter filter;
|
||||
|
||||
private Integer timeoutSeconds;
|
||||
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
private Boolean includeDistinctCount = false;
|
||||
|
||||
@ -51,6 +53,17 @@ public class CountInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public CountInput(String tableName)
|
||||
{
|
||||
setTableName(tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filter
|
||||
**
|
||||
@ -152,4 +165,46 @@ public class CountInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for filter
|
||||
*******************************************************************************/
|
||||
public CountInput withFilter(QQueryFilter filter)
|
||||
{
|
||||
this.filter = filter;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public Integer getTimeoutSeconds()
|
||||
{
|
||||
return (this.timeoutSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public void setTimeoutSeconds(Integer timeoutSeconds)
|
||||
{
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public CountInput withTimeoutSeconds(Integer timeoutSeconds)
|
||||
{
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -43,6 +43,9 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
private QQueryFilter queryFilter;
|
||||
private InputSource inputSource = QInputSource.SYSTEM;
|
||||
|
||||
private boolean omitDmlAudit = false;
|
||||
private String auditContext = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -54,6 +57,29 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DeleteInput(String tableName)
|
||||
{
|
||||
setTableName(tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public DeleteInput withTableName(String tableName)
|
||||
{
|
||||
super.withTableName(tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for transaction
|
||||
**
|
||||
@ -188,4 +214,66 @@ public class DeleteInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for omitDmlAudit
|
||||
*******************************************************************************/
|
||||
public boolean getOmitDmlAudit()
|
||||
{
|
||||
return (this.omitDmlAudit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for omitDmlAudit
|
||||
*******************************************************************************/
|
||||
public void setOmitDmlAudit(boolean omitDmlAudit)
|
||||
{
|
||||
this.omitDmlAudit = omitDmlAudit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for omitDmlAudit
|
||||
*******************************************************************************/
|
||||
public DeleteInput withOmitDmlAudit(boolean omitDmlAudit)
|
||||
{
|
||||
this.omitDmlAudit = omitDmlAudit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for auditContext
|
||||
*******************************************************************************/
|
||||
public String getAuditContext()
|
||||
{
|
||||
return (this.auditContext);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for auditContext
|
||||
*******************************************************************************/
|
||||
public void setAuditContext(String auditContext)
|
||||
{
|
||||
this.auditContext = auditContext;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for auditContext
|
||||
*******************************************************************************/
|
||||
public DeleteInput withAuditContext(String auditContext)
|
||||
{
|
||||
this.auditContext = auditContext;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,17 +23,21 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
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.actions.tables.QueryOrGetInputInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input data for the Get action
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class GetInput extends AbstractTableActionInput
|
||||
public class GetInput extends AbstractTableActionInput implements QueryOrGetInputInterface
|
||||
{
|
||||
private QBackendTransaction transaction;
|
||||
|
||||
@ -46,6 +50,7 @@ public class GetInput extends AbstractTableActionInput
|
||||
private boolean shouldOmitHiddenFields = true;
|
||||
private boolean shouldMaskPasswords = true;
|
||||
|
||||
private List<QueryJoin> queryJoins = null;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
|
||||
@ -66,6 +71,29 @@ public class GetInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public GetInput(String tableName)
|
||||
{
|
||||
setTableName(tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public AbstractTableActionInput withTableName(String tableName)
|
||||
{
|
||||
super.withTableName(tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for primaryKey
|
||||
**
|
||||
@ -387,4 +415,51 @@ public class GetInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryJoins
|
||||
*******************************************************************************/
|
||||
public List<QueryJoin> getQueryJoins()
|
||||
{
|
||||
return (this.queryJoins);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for queryJoins
|
||||
*******************************************************************************/
|
||||
public void setQueryJoins(List<QueryJoin> queryJoins)
|
||||
{
|
||||
this.queryJoins = queryJoins;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryJoins
|
||||
*******************************************************************************/
|
||||
public GetInput withQueryJoins(List<QueryJoin> queryJoins)
|
||||
{
|
||||
this.queryJoins = queryJoins;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for queryJoins
|
||||
**
|
||||
*******************************************************************************/
|
||||
public GetInput withQueryJoin(QueryJoin queryJoin)
|
||||
{
|
||||
if(this.queryJoins == null)
|
||||
{
|
||||
this.queryJoins = new ArrayList<>();
|
||||
}
|
||||
this.queryJoins.add(queryJoin);
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,12 +22,15 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -43,6 +46,7 @@ public class InsertInput extends AbstractTableActionInput
|
||||
private boolean skipUniqueKeyCheck = false;
|
||||
|
||||
private boolean omitDmlAudit = false;
|
||||
private String auditContext = null;
|
||||
|
||||
|
||||
|
||||
@ -55,6 +59,71 @@ public class InsertInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput(String tableName)
|
||||
{
|
||||
setTableName(tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public InsertInput withTableName(String tableName)
|
||||
{
|
||||
super.withTableName(tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput withRecord(QRecord record)
|
||||
{
|
||||
if(records == null)
|
||||
{
|
||||
records = new ArrayList<>();
|
||||
}
|
||||
|
||||
records.add(record);
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput withRecordEntity(QRecordEntity recordEntity)
|
||||
{
|
||||
return (withRecord(recordEntity.toQRecord()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput withRecordEntities(List<QRecordEntity> recordEntityList)
|
||||
{
|
||||
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
|
||||
{
|
||||
withRecordEntity(recordEntity);
|
||||
}
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for transaction
|
||||
**
|
||||
@ -216,4 +285,35 @@ public class InsertInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for auditContext
|
||||
*******************************************************************************/
|
||||
public String getAuditContext()
|
||||
{
|
||||
return (this.auditContext);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for auditContext
|
||||
*******************************************************************************/
|
||||
public void setAuditContext(String auditContext)
|
||||
{
|
||||
this.auditContext = auditContext;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for auditContext
|
||||
*******************************************************************************/
|
||||
public InsertInput withAuditContext(String auditContext)
|
||||
{
|
||||
this.auditContext = auditContext;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,17 +30,21 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
|
||||
import org.apache.logging.log4j.Level;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
@ -60,6 +64,7 @@ public class JoinsContext
|
||||
// note - will have entries for all tables, not just aliases. //
|
||||
////////////////////////////////////////////////////////////////
|
||||
private final Map<String, String> aliasToTableNameMap = new HashMap<>();
|
||||
private Level logLevel = Level.OFF;
|
||||
|
||||
|
||||
|
||||
@ -69,62 +74,23 @@ public class JoinsContext
|
||||
*******************************************************************************/
|
||||
public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
|
||||
{
|
||||
log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
|
||||
this.instance = instance;
|
||||
this.mainTableName = tableName;
|
||||
this.queryJoins = new MutableList<>(queryJoins);
|
||||
|
||||
for(QueryJoin queryJoin : this.queryJoins)
|
||||
{
|
||||
log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
|
||||
processQueryJoin(queryJoin);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////
|
||||
// ensure any joins that contribute a recordLock are present //
|
||||
///////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
|
||||
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = instance.getTable(mainTableName);
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
if(this.queryJoins.stream().anyMatch(queryJoin ->
|
||||
{
|
||||
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
|
||||
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
|
||||
}))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin);
|
||||
tmpTable = instance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin); //
|
||||
tmpTable = instance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
|
||||
}
|
||||
}
|
||||
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
|
||||
}
|
||||
|
||||
ensureFilterIsRepresented(filter);
|
||||
@ -141,6 +107,86 @@ public class JoinsContext
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
|
||||
log("--- END ------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
|
||||
|
||||
QTableMetaData tmpTable = instance.getTable(mainTableName);
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check the joins currently in the query - if any are for this table, then we don't need to add one //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QueryJoin> matchingJoins = this.queryJoins.stream().filter(queryJoin ->
|
||||
{
|
||||
QJoinMetaData joinMetaData = null;
|
||||
if(queryJoin.getJoinMetaData() != null)
|
||||
{
|
||||
joinMetaData = queryJoin.getJoinMetaData();
|
||||
}
|
||||
else
|
||||
{
|
||||
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
|
||||
}
|
||||
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
|
||||
}).toList();
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(matchingJoins))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
|
||||
// todo - is this always right? what about nulls-allowed? //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
log("- skipping join already in the query", logPair("joinName", joinName));
|
||||
|
||||
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
|
||||
{
|
||||
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
|
||||
matchingJoins.get(0).setType(QueryJoin.Type.INNER);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
|
||||
tmpTable = instance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
|
||||
tmpTable = instance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -151,8 +197,15 @@ public class JoinsContext
|
||||
** use this method to add to the list, instead of ever adding directly, as it's
|
||||
** important do to that process step (and we've had bugs when it wasn't done).
|
||||
*******************************************************************************/
|
||||
private void addQueryJoin(QueryJoin queryJoin) throws QException
|
||||
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
|
||||
{
|
||||
log("Adding query join to context",
|
||||
logPair("reason", reason),
|
||||
logPair("joinTable", queryJoin.getJoinTable()),
|
||||
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
|
||||
logPair("joinMetaData.leftTable", () -> queryJoin.getJoinMetaData().getLeftTable()),
|
||||
logPair("joinMetaData.rightTable", () -> queryJoin.getJoinMetaData().getRightTable())
|
||||
);
|
||||
this.queryJoins.add(queryJoin);
|
||||
processQueryJoin(queryJoin);
|
||||
}
|
||||
@ -177,10 +230,46 @@ public class JoinsContext
|
||||
addedJoin = false;
|
||||
for(QueryJoin queryJoin : queryJoins)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
// if the join has joinMetaData, then we don't need to process it. //
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
if(queryJoin.getJoinMetaData() == null)
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
|
||||
if(joinMetaData != null)
|
||||
{
|
||||
boolean isJoinLeftTableInQuery = false;
|
||||
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
|
||||
if(joinMetaDataLeftTable.equals(mainTableName))
|
||||
{
|
||||
isJoinLeftTableInQuery = true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// check the other joins in this query - if any of them have this join's left-table as their baseTable, then set the flag to true //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QueryJoin otherJoin : queryJoins)
|
||||
{
|
||||
if(otherJoin == queryJoin)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(Objects.equals(otherJoin.getBaseTableOrAlias(), joinMetaDataLeftTable))
|
||||
{
|
||||
isJoinLeftTableInQuery = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// if the join's left-table isn't in the query, then we need to flip the join. //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
if(!isJoinLeftTableInQuery)
|
||||
{
|
||||
log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
|
||||
queryJoin.setJoinMetaData(joinMetaData.flip());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// try to find a direct join between the main table and this table. //
|
||||
@ -190,6 +279,7 @@ public class JoinsContext
|
||||
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
|
||||
if(found != null)
|
||||
{
|
||||
log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
|
||||
queryJoin.setJoinMetaData(found);
|
||||
}
|
||||
else
|
||||
@ -197,15 +287,13 @@ public class JoinsContext
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, the join must be indirect - so look for an exposedJoin that will have a joinPath that will connect us //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
LOG.debug("Looking for an exposed join...", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()));
|
||||
|
||||
QTableMetaData mainTable = instance.getTable(mainTableName);
|
||||
boolean addedAnyQueryJoins = false;
|
||||
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins()))
|
||||
{
|
||||
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
|
||||
{
|
||||
LOG.debug("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
|
||||
log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// loop backward through the join path (from the joinTable back to the main table) //
|
||||
@ -250,7 +338,7 @@ public class JoinsContext
|
||||
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
|
||||
queryJoinToAdd.setType(queryJoin.getType());
|
||||
addedAnyQueryJoins = true;
|
||||
this.addQueryJoin(queryJoinToAdd);
|
||||
this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,9 +465,9 @@ public class JoinsContext
|
||||
**
|
||||
** e.g., Given:
|
||||
** FROM `order` INNER JOIN line_item li
|
||||
** hasAliasOrTable("order") => true
|
||||
** hasAliasOrTable("li") => false
|
||||
** hasAliasOrTable("line_item") => true
|
||||
** hasTable("order") => true
|
||||
** hasTable("li") => false
|
||||
** hasTable("line_item") => true
|
||||
*******************************************************************************/
|
||||
public boolean hasTable(String table)
|
||||
{
|
||||
@ -415,15 +503,17 @@ public class JoinsContext
|
||||
|
||||
for(String filterTable : filterTables)
|
||||
{
|
||||
log("Evaluating filterTable", logPair("filterTable", filterTable));
|
||||
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
|
||||
{
|
||||
log("- table not in query - adding it", logPair("filterTable", filterTable));
|
||||
boolean found = false;
|
||||
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
|
||||
{
|
||||
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
|
||||
if(queryJoin != null)
|
||||
{
|
||||
this.addQueryJoin(queryJoin);
|
||||
this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@ -432,7 +522,7 @@ public class JoinsContext
|
||||
if(!found)
|
||||
{
|
||||
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
|
||||
this.addQueryJoin(queryJoin);
|
||||
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -569,4 +659,14 @@ public class JoinsContext
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void log(String message, LogPair... logPairs)
|
||||
{
|
||||
LOG.log(logLevel, message, null, logPairs);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -26,8 +26,9 @@ import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
@ -36,6 +37,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
* A single criteria Component of a Query
|
||||
*
|
||||
*******************************************************************************/
|
||||
@JsonDeserialize(using = QFilterCriteriaDeserializer.class)
|
||||
public class QFilterCriteria implements Serializable, Cloneable
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
|
||||
@ -44,8 +46,10 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
private QCriteriaOperator operator;
|
||||
private List<Serializable> values;
|
||||
|
||||
private String otherFieldName;
|
||||
private AbstractFilterExpression<?> expression;
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - probably implement this as a type of expression - though would require a little special handling i think when evaluating... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
private String otherFieldName;
|
||||
|
||||
|
||||
|
||||
@ -98,23 +102,6 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
this.operator = operator;
|
||||
this.expression = expression;
|
||||
|
||||
///////////////////////////////////////
|
||||
// this guy doesn't like to be null? //
|
||||
///////////////////////////////////////
|
||||
this.values = new ArrayList<>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -332,35 +319,4 @@ public class QFilterCriteria implements Serializable, Cloneable
|
||||
return (rs.toString());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for expression
|
||||
*******************************************************************************/
|
||||
public AbstractFilterExpression<?> getExpression()
|
||||
{
|
||||
return (this.expression);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for expression
|
||||
*******************************************************************************/
|
||||
public void setExpression(AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for expression
|
||||
*******************************************************************************/
|
||||
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
|
||||
{
|
||||
this.expression = expression;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,18 +29,20 @@ import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input data for the Query action
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QueryInput extends AbstractTableActionInput
|
||||
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface
|
||||
{
|
||||
private QBackendTransaction transaction;
|
||||
private QQueryFilter filter;
|
||||
|
||||
private RecordPipe recordPipe;
|
||||
private Integer timeoutSeconds;
|
||||
|
||||
private boolean shouldTranslatePossibleValues = false;
|
||||
private boolean shouldGenerateDisplayValues = false;
|
||||
@ -77,6 +79,17 @@ public class QueryInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QueryInput(String tableName)
|
||||
{
|
||||
setTableName(tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filter
|
||||
**
|
||||
@ -525,4 +538,35 @@ public class QueryInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public Integer getTimeoutSeconds()
|
||||
{
|
||||
return (this.timeoutSeconds);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public void setTimeoutSeconds(Integer timeoutSeconds)
|
||||
{
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for timeoutSeconds
|
||||
*******************************************************************************/
|
||||
public QueryInput withTimeoutSeconds(Integer timeoutSeconds)
|
||||
{
|
||||
this.timeoutSeconds = timeoutSeconds;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,10 +28,31 @@ import java.io.Serializable;
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractFilterExpression<T extends Serializable>
|
||||
public abstract class AbstractFilterExpression<T extends Serializable> implements Serializable
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract T evaluate();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** To help with serialization, define a "type" in all subclasses
|
||||
*******************************************************************************/
|
||||
public String getType()
|
||||
{
|
||||
return (getClass().getSimpleName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** noop - but here so serialization won't be upset about there being a type
|
||||
** in a json object.
|
||||
*******************************************************************************/
|
||||
public void setType(String type)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@ -31,9 +35,9 @@ import java.util.concurrent.TimeUnit;
|
||||
*******************************************************************************/
|
||||
public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
{
|
||||
private final Operator operator;
|
||||
private final int amount;
|
||||
private final TimeUnit timeUnit;
|
||||
private Operator operator;
|
||||
private int amount;
|
||||
private ChronoUnit timeUnit;
|
||||
|
||||
|
||||
|
||||
@ -46,7 +50,17 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit)
|
||||
public NowWithOffset()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private NowWithOffset(Operator operator, int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
this.operator = operator;
|
||||
this.amount = amount;
|
||||
@ -59,7 +73,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated
|
||||
public static NowWithOffset minus(int amount, TimeUnit timeUnit)
|
||||
{
|
||||
return (minus(amount, timeUnit.toChronoUnit()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static NowWithOffset minus(int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
|
||||
}
|
||||
@ -70,7 +96,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Deprecated
|
||||
public static NowWithOffset plus(int amount, TimeUnit timeUnit)
|
||||
{
|
||||
return (plus(amount, timeUnit.toChronoUnit()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static NowWithOffset plus(int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
|
||||
}
|
||||
@ -83,14 +121,24 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
|
||||
// but LocalDateTime does. So, make a LDT in UTC, do the plus/minus, then //
|
||||
// convert back to Instant @ UTC //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
|
||||
|
||||
LocalDateTime then;
|
||||
if(operator.equals(Operator.PLUS))
|
||||
{
|
||||
return (Instant.now().plus(amount, timeUnit.toChronoUnit()));
|
||||
then = now.plus(amount, timeUnit);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (Instant.now().minus(amount, timeUnit.toChronoUnit()));
|
||||
then = now.minus(amount, timeUnit);
|
||||
}
|
||||
|
||||
return (then.toInstant(ZoneOffset.UTC));
|
||||
}
|
||||
|
||||
|
||||
@ -121,7 +169,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
|
||||
** Getter for timeUnit
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TimeUnit getTimeUnit()
|
||||
public ChronoUnit getTimeUnit()
|
||||
{
|
||||
return timeUnit;
|
||||
}
|
||||
|
@ -0,0 +1,178 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
|
||||
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoField;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
|
||||
{
|
||||
private Operator operator;
|
||||
private ChronoUnit timeUnit;
|
||||
|
||||
|
||||
|
||||
public enum Operator
|
||||
{THIS, LAST}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ThisOrLastPeriod()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
private ThisOrLastPeriod(Operator operator, ChronoUnit timeUnit)
|
||||
{
|
||||
this.operator = operator;
|
||||
this.timeUnit = timeUnit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ThisOrLastPeriod this_(ChronoUnit timeUnit)
|
||||
{
|
||||
return (new ThisOrLastPeriod(Operator.THIS, timeUnit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Factory
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit)
|
||||
{
|
||||
return (new ThisOrLastPeriod(Operator.LAST, timeUnit));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public Instant evaluate()
|
||||
{
|
||||
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
|
||||
|
||||
switch(timeUnit)
|
||||
{
|
||||
case HOURS ->
|
||||
{
|
||||
if(operator.equals(Operator.THIS))
|
||||
{
|
||||
return Instant.now().truncatedTo(ChronoUnit.HOURS);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
|
||||
}
|
||||
}
|
||||
case DAYS ->
|
||||
{
|
||||
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
|
||||
return operator.equals(Operator.THIS) ? startOfToday : startOfToday.minus(1, ChronoUnit.DAYS);
|
||||
}
|
||||
case WEEKS ->
|
||||
{
|
||||
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
|
||||
LocalDateTime startOfThisWeekLDT = LocalDateTime.ofInstant(startOfToday, zoneId);
|
||||
while(startOfThisWeekLDT.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// go backwards until sunday is found //
|
||||
////////////////////////////////////////
|
||||
startOfThisWeekLDT = startOfThisWeekLDT.minus(1, ChronoUnit.DAYS);
|
||||
}
|
||||
Instant startOfThisWeek = startOfThisWeekLDT.toInstant(zoneId.getRules().getOffset(startOfThisWeekLDT));
|
||||
|
||||
return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minus(7, ChronoUnit.DAYS);
|
||||
}
|
||||
case MONTHS ->
|
||||
{
|
||||
Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId());
|
||||
LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId()));
|
||||
LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minus(1, ChronoUnit.MONTHS);
|
||||
Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now()));
|
||||
|
||||
return operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth;
|
||||
}
|
||||
case YEARS ->
|
||||
{
|
||||
Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId());
|
||||
LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId);
|
||||
LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minus(1, ChronoUnit.YEARS);
|
||||
Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now()));
|
||||
|
||||
return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear;
|
||||
}
|
||||
default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for operator
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Operator getOperator()
|
||||
{
|
||||
return operator;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timeUnit
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ChronoUnit getTimeUnit()
|
||||
{
|
||||
return timeUnit;
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.core.JacksonException;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
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.expressions.AbstractFilterExpression;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Custom jackson deserializer, to deal w/ abstract expression field
|
||||
*******************************************************************************/
|
||||
public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria>
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteriaDeserializer()
|
||||
{
|
||||
this(null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFilterCriteriaDeserializer(Class<?> vc)
|
||||
{
|
||||
super(vc);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public QFilterCriteria deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
|
||||
{
|
||||
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/////////////////////////////////
|
||||
// get values out of json node //
|
||||
/////////////////////////////////
|
||||
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
|
||||
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
|
||||
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
|
||||
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// look at all the values - if any of them are actually meant to be an Expression (instance of subclass of AbstractFilterExpression) //
|
||||
// they'll have deserialized as a Map, with a "type" key. If that's the case, then re/de serialize them into the proper expression type //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListIterator<Serializable> valuesIterator = CollectionUtils.nonNullList(values).listIterator();
|
||||
while(valuesIterator.hasNext())
|
||||
{
|
||||
Object value = valuesIterator.next();
|
||||
if(value instanceof Map<?, ?> map && map.containsKey("type"))
|
||||
{
|
||||
String expressionType = ValueUtils.getValueAsString(map.get("type"));
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// right now, we'll assume that all expression subclasses are in the same package as AbstractFilterExpression //
|
||||
// so, we can just do a Class.forName on that name, and use JsonUtils.toObject requesting that class. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String assumedExpressionJSON = JsonUtils.toJson(map);
|
||||
|
||||
String className = AbstractFilterExpression.class.getName().replace(AbstractFilterExpression.class.getSimpleName(), expressionType);
|
||||
Serializable replacementValue = (Serializable) JsonUtils.toObject(assumedExpressionJSON, Class.forName(className));
|
||||
valuesIterator.set(replacementValue);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new IOException("Error deserializing criteria value which appeared to be an expression of type [" + expressionType + "] inside QFilterCriteria", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////
|
||||
// put fields into return object //
|
||||
///////////////////////////////////
|
||||
QFilterCriteria criteria = new QFilterCriteria();
|
||||
criteria.setFieldName(fieldName);
|
||||
criteria.setOperator(operator);
|
||||
criteria.setValues(values);
|
||||
criteria.setOtherFieldName(otherFieldName);
|
||||
|
||||
return (criteria);
|
||||
}
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ReplaceInput extends AbstractTableActionInput
|
||||
{
|
||||
private QBackendTransaction transaction;
|
||||
private UniqueKey key;
|
||||
private List<QRecord> records;
|
||||
private QQueryFilter filter;
|
||||
|
||||
private boolean omitDmlAudit = false;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReplaceInput()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for transaction
|
||||
*******************************************************************************/
|
||||
public QBackendTransaction getTransaction()
|
||||
{
|
||||
return (this.transaction);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for transaction
|
||||
*******************************************************************************/
|
||||
public void setTransaction(QBackendTransaction transaction)
|
||||
{
|
||||
this.transaction = transaction;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for transaction
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withTransaction(QBackendTransaction transaction)
|
||||
{
|
||||
this.transaction = transaction;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for records
|
||||
*******************************************************************************/
|
||||
public List<QRecord> getRecords()
|
||||
{
|
||||
return (this.records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for records
|
||||
*******************************************************************************/
|
||||
public void setRecords(List<QRecord> records)
|
||||
{
|
||||
this.records = records;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for records
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withRecords(List<QRecord> records)
|
||||
{
|
||||
this.records = records;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filter
|
||||
*******************************************************************************/
|
||||
public QQueryFilter getFilter()
|
||||
{
|
||||
return (this.filter);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for filter
|
||||
*******************************************************************************/
|
||||
public void setFilter(QQueryFilter filter)
|
||||
{
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for filter
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withFilter(QQueryFilter filter)
|
||||
{
|
||||
this.filter = filter;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for key
|
||||
*******************************************************************************/
|
||||
public UniqueKey getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for key
|
||||
*******************************************************************************/
|
||||
public void setKey(UniqueKey key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for key
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withKey(UniqueKey key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for omitDmlAudit
|
||||
*******************************************************************************/
|
||||
public boolean getOmitDmlAudit()
|
||||
{
|
||||
return (this.omitDmlAudit);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for omitDmlAudit
|
||||
*******************************************************************************/
|
||||
public void setOmitDmlAudit(boolean omitDmlAudit)
|
||||
{
|
||||
this.omitDmlAudit = omitDmlAudit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for omitDmlAudit
|
||||
*******************************************************************************/
|
||||
public ReplaceInput withOmitDmlAudit(boolean omitDmlAudit)
|
||||
{
|
||||
this.omitDmlAudit = omitDmlAudit;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ReplaceOutput extends AbstractActionOutput
|
||||
{
|
||||
private InsertOutput insertOutput;
|
||||
private UpdateOutput updateOutput;
|
||||
private DeleteOutput deleteOutput;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for insertOutput
|
||||
*******************************************************************************/
|
||||
public InsertOutput getInsertOutput()
|
||||
{
|
||||
return (this.insertOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for insertOutput
|
||||
*******************************************************************************/
|
||||
public void setInsertOutput(InsertOutput insertOutput)
|
||||
{
|
||||
this.insertOutput = insertOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for insertOutput
|
||||
*******************************************************************************/
|
||||
public ReplaceOutput withInsertOutput(InsertOutput insertOutput)
|
||||
{
|
||||
this.insertOutput = insertOutput;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for updateOutput
|
||||
*******************************************************************************/
|
||||
public UpdateOutput getUpdateOutput()
|
||||
{
|
||||
return (this.updateOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for updateOutput
|
||||
*******************************************************************************/
|
||||
public void setUpdateOutput(UpdateOutput updateOutput)
|
||||
{
|
||||
this.updateOutput = updateOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for updateOutput
|
||||
*******************************************************************************/
|
||||
public ReplaceOutput withUpdateOutput(UpdateOutput updateOutput)
|
||||
{
|
||||
this.updateOutput = updateOutput;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for deleteOutput
|
||||
*******************************************************************************/
|
||||
public DeleteOutput getDeleteOutput()
|
||||
{
|
||||
return (this.deleteOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for deleteOutput
|
||||
*******************************************************************************/
|
||||
public void setDeleteOutput(DeleteOutput deleteOutput)
|
||||
{
|
||||
this.deleteOutput = deleteOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for deleteOutput
|
||||
*******************************************************************************/
|
||||
public ReplaceOutput withDeleteOutput(DeleteOutput deleteOutput)
|
||||
{
|
||||
this.deleteOutput = deleteOutput;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -22,12 +22,15 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.update;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -62,6 +65,71 @@ public class UpdateInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public UpdateInput(String tableName)
|
||||
{
|
||||
setTableName(tableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public UpdateInput withTableName(String tableName)
|
||||
{
|
||||
super.withTableName(tableName);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public UpdateInput withRecord(QRecord record)
|
||||
{
|
||||
if(records == null)
|
||||
{
|
||||
records = new ArrayList<>();
|
||||
}
|
||||
|
||||
records.add(record);
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public UpdateInput withRecordEntity(QRecordEntity recordEntity)
|
||||
{
|
||||
return (withRecord(recordEntity.toQRecord()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public UpdateInput withRecordEntities(List<QRecordEntity> recordEntityList)
|
||||
{
|
||||
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
|
||||
{
|
||||
withRecordEntity(recordEntity);
|
||||
}
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for transaction
|
||||
**
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.templates;
|
||||
package com.kingsrook.qqq.backend.core.model.actions.templates;
|
||||
|
||||
|
||||
import java.io.OutputStream;
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.templates;
|
||||
package com.kingsrook.qqq.backend.core.model.actions.templates;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@ -19,11 +19,12 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.templates;
|
||||
package com.kingsrook.qqq.backend.core.model.actions.templates;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -35,7 +36,7 @@ public class RenderTemplateInput extends AbstractActionInput
|
||||
private String code; // todo - TemplateReference, like CodeReference??
|
||||
private TemplateType templateType;
|
||||
|
||||
private Map<String, Object> context;
|
||||
private Map<String, ? extends Object> context;
|
||||
|
||||
|
||||
|
||||
@ -120,7 +121,7 @@ public class RenderTemplateInput extends AbstractActionInput
|
||||
** Getter for context
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, Object> getContext()
|
||||
public Map<String, ? extends Object> getContext()
|
||||
{
|
||||
return context;
|
||||
}
|
||||
@ -131,7 +132,7 @@ public class RenderTemplateInput extends AbstractActionInput
|
||||
** Setter for context
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setContext(Map<String, Object> context)
|
||||
public void setContext(Map<String, ? extends Object> context)
|
||||
{
|
||||
this.context = context;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.templates;
|
||||
package com.kingsrook.qqq.backend.core.model.actions.templates;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
@ -50,6 +50,17 @@ public class RenderWidgetInput extends AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getActionIdentity()
|
||||
{
|
||||
return (getClass().getSimpleName() + ":" + widgetMetaData.getName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for widgetMetaData
|
||||
**
|
||||
|
@ -116,16 +116,19 @@ public class AuditsMetaDataProvider
|
||||
instance.addPossibleValueSource(new QPossibleValueSource()
|
||||
.withName(TABLE_NAME_AUDIT_TABLE)
|
||||
.withTableName(TABLE_NAME_AUDIT_TABLE)
|
||||
.withOrderByField("name")
|
||||
);
|
||||
|
||||
instance.addPossibleValueSource(new QPossibleValueSource()
|
||||
.withName(TABLE_NAME_AUDIT_USER)
|
||||
.withTableName(TABLE_NAME_AUDIT_USER)
|
||||
.withOrderByField("name")
|
||||
);
|
||||
|
||||
instance.addPossibleValueSource(new QPossibleValueSource()
|
||||
.withName(TABLE_NAME_AUDIT)
|
||||
.withTableName(TABLE_NAME_AUDIT)
|
||||
.withOrderByField("id", false)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.scripts.Script;
|
||||
|
||||
|
||||
@ -50,7 +51,7 @@ public class TableTrigger extends QRecordEntity
|
||||
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
|
||||
private String tableName;
|
||||
|
||||
@QField(/* todo possibleValueSourceName = */)
|
||||
@QField(possibleValueSourceName = SavedFilter.TABLE_NAME)
|
||||
private Integer filterId;
|
||||
|
||||
@QField(possibleValueSourceName = Script.TABLE_NAME)
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.data;
|
||||
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Annotation to place onto fields in a QRecordEntity, to mark them as associated
|
||||
** record lists
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Target(ElementType.FIELD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface QAssociation
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String name();
|
||||
|
||||
}
|
@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.model.data;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.AnnotatedParameterizedType;
|
||||
import java.lang.reflect.AnnotatedType;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
@ -40,7 +44,9 @@ import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -50,7 +56,8 @@ public abstract class QRecordEntity
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QRecordEntity.class);
|
||||
|
||||
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>();
|
||||
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>();
|
||||
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityAssociation> associationMapping = new ListingHash<>();
|
||||
|
||||
private Map<String, Serializable> originalRecordValues;
|
||||
|
||||
@ -61,11 +68,23 @@ public abstract class QRecordEntity
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord) throws QException
|
||||
{
|
||||
return (QRecordEntity.fromQRecord(c, qRecord, ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build an entity of this QRecord type from a QRecord - where the fields for
|
||||
** this entity have the given prefix - e.g., if they were selected as part of a join.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord, String fieldNamePrefix) throws QException
|
||||
{
|
||||
try
|
||||
{
|
||||
T entity = c.getConstructor().newInstance();
|
||||
entity.populateFromQRecord(qRecord);
|
||||
entity.populateFromQRecord(qRecord, fieldNamePrefix);
|
||||
return (entity);
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -80,19 +99,73 @@ public abstract class QRecordEntity
|
||||
** Build an entity of this QRecord type from a QRecord
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord) throws QRuntimeException
|
||||
protected void populateFromQRecord(QRecord qRecord) throws QRuntimeException
|
||||
{
|
||||
populateFromQRecord(qRecord, "");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Build an entity of this QRecord type from a QRecord - where the fields for
|
||||
** this entity have the given prefix - e.g., if they were selected as part of a join.
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord, String fieldNamePrefix) throws QRuntimeException
|
||||
{
|
||||
try
|
||||
{
|
||||
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
|
||||
originalRecordValues = new HashMap<>();
|
||||
|
||||
if(fieldNamePrefix == null)
|
||||
{
|
||||
fieldNamePrefix = "";
|
||||
}
|
||||
|
||||
for(QRecordEntityField qRecordEntityField : fieldList)
|
||||
{
|
||||
Serializable value = qRecord.getValue(qRecordEntityField.getFieldName());
|
||||
Serializable value = qRecord.getValue(fieldNamePrefix + qRecordEntityField.getFieldName());
|
||||
Object typedValue = qRecordEntityField.convertValueType(value);
|
||||
qRecordEntityField.getSetter().invoke(this, typedValue);
|
||||
originalRecordValues.put(qRecordEntityField.getFieldName(), value);
|
||||
}
|
||||
|
||||
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
|
||||
{
|
||||
List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
|
||||
if(associatedRecords == null)
|
||||
{
|
||||
qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<QRecordEntity> associatedEntityList = new ArrayList<>();
|
||||
for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
|
||||
{
|
||||
associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
|
||||
}
|
||||
qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
|
||||
}
|
||||
}
|
||||
|
||||
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
|
||||
{
|
||||
List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
|
||||
if(associatedRecords == null)
|
||||
{
|
||||
qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<QRecordEntity> associatedEntityList = new ArrayList<>();
|
||||
for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
|
||||
{
|
||||
associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
|
||||
}
|
||||
qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@ -112,12 +185,30 @@ public abstract class QRecordEntity
|
||||
{
|
||||
QRecord qRecord = new QRecord();
|
||||
|
||||
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
|
||||
for(QRecordEntityField qRecordEntityField : fieldList)
|
||||
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
|
||||
{
|
||||
qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
|
||||
}
|
||||
|
||||
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
|
||||
{
|
||||
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
|
||||
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
|
||||
|
||||
if(associatedEntities != null)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// do this so an empty list in the entity becomes an empty list in the QRecord //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
|
||||
}
|
||||
|
||||
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
|
||||
{
|
||||
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
|
||||
}
|
||||
}
|
||||
|
||||
return (qRecord);
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -127,7 +218,6 @@ public abstract class QRecordEntity
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -137,8 +227,7 @@ public abstract class QRecordEntity
|
||||
{
|
||||
QRecord qRecord = new QRecord();
|
||||
|
||||
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
|
||||
for(QRecordEntityField qRecordEntityField : fieldList)
|
||||
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
|
||||
{
|
||||
Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this);
|
||||
Serializable originalValue = null;
|
||||
@ -153,6 +242,25 @@ public abstract class QRecordEntity
|
||||
}
|
||||
}
|
||||
|
||||
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
|
||||
{
|
||||
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
|
||||
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
|
||||
|
||||
if(associatedEntities != null)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// do this so an empty list in the entity becomes an empty list in the QRecord //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
|
||||
}
|
||||
|
||||
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
|
||||
{
|
||||
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
|
||||
}
|
||||
}
|
||||
|
||||
return (qRecord);
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -181,7 +289,15 @@ public abstract class QRecordEntity
|
||||
{
|
||||
String fieldName = getFieldNameFromGetter(possibleGetter);
|
||||
Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName);
|
||||
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
|
||||
|
||||
if(fieldAnnotation.isPresent())
|
||||
{
|
||||
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -196,15 +312,73 @@ public abstract class QRecordEntity
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static List<QRecordEntityAssociation> getAssociationList(Class<? extends QRecordEntity> c)
|
||||
{
|
||||
if(!associationMapping.containsKey(c))
|
||||
{
|
||||
List<QRecordEntityAssociation> associationList = new ArrayList<>();
|
||||
for(Method possibleGetter : c.getMethods())
|
||||
{
|
||||
if(isGetter(possibleGetter))
|
||||
{
|
||||
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
|
||||
|
||||
if(setter.isPresent())
|
||||
{
|
||||
String fieldName = getFieldNameFromGetter(possibleGetter);
|
||||
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
|
||||
|
||||
if(associationAnnotation.isPresent())
|
||||
{
|
||||
Class<? extends QRecordEntity> listTypeParam = (Class<? extends QRecordEntity>) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
|
||||
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
|
||||
}
|
||||
}
|
||||
}
|
||||
associationMapping.put(c, associationList);
|
||||
}
|
||||
return (associationMapping.get(c));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<QField> getQFieldAnnotation(Class<? extends QRecordEntity> c, String fieldName)
|
||||
{
|
||||
return (getAnnotationOnField(c, QField.class, fieldName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<QAssociation> getQAssociationAnnotation(Class<? extends QRecordEntity> c, String fieldName)
|
||||
{
|
||||
return (getAnnotationOnField(c, QAssociation.class, fieldName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <A extends Annotation> Optional<A> getAnnotationOnField(Class<? extends QRecordEntity> c, Class<A> annotationClass, String fieldName)
|
||||
{
|
||||
try
|
||||
{
|
||||
Field field = c.getDeclaredField(fieldName);
|
||||
return (Optional.ofNullable(field.getAnnotation(QField.class)));
|
||||
return (Optional.ofNullable(field.getAnnotation(annotationClass)));
|
||||
}
|
||||
catch(NoSuchFieldException e)
|
||||
{
|
||||
@ -239,7 +413,7 @@ public abstract class QRecordEntity
|
||||
{
|
||||
if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
|
||||
{
|
||||
if(isSupportedFieldType(method.getReturnType()))
|
||||
if(isSupportedFieldType(method.getReturnType()) || isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType()))
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
@ -247,7 +421,7 @@ public abstract class QRecordEntity
|
||||
{
|
||||
if(!method.getName().equals("getClass"))
|
||||
{
|
||||
LOG.info("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
|
||||
LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -304,4 +478,41 @@ public abstract class QRecordEntity
|
||||
/////////////////////////////////////////////
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static boolean isSupportedAssociation(Class<?> returnType, AnnotatedType annotatedType)
|
||||
{
|
||||
Class<?> listTypeParam = getListTypeParam(returnType, annotatedType);
|
||||
return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Class<?> getListTypeParam(Class<?> listType, AnnotatedType annotatedType)
|
||||
{
|
||||
if(listType.equals(List.class))
|
||||
{
|
||||
if(annotatedType instanceof AnnotatedParameterizedType apt)
|
||||
{
|
||||
AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments();
|
||||
for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments)
|
||||
{
|
||||
Type type = annotatedActualTypeArgument.getType();
|
||||
if(type instanceof Class<?> c)
|
||||
{
|
||||
return (c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.data;
|
||||
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Reflective information about an association in a QRecordEntity
|
||||
*******************************************************************************/
|
||||
public class QRecordEntityAssociation
|
||||
{
|
||||
private final String fieldName;
|
||||
private final Method getter;
|
||||
private final Method setter;
|
||||
|
||||
private final Class<? extends QRecordEntity> associatedType;
|
||||
|
||||
private final QAssociation associationAnnotation;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor.
|
||||
*******************************************************************************/
|
||||
public QRecordEntityAssociation(String fieldName, Method getter, Method setter, Class<? extends QRecordEntity> associatedType, QAssociation associationAnnotation)
|
||||
{
|
||||
this.fieldName = fieldName;
|
||||
this.getter = getter;
|
||||
this.setter = setter;
|
||||
this.associatedType = associatedType;
|
||||
this.associationAnnotation = associationAnnotation;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for fieldName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getFieldName()
|
||||
{
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for getter
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Method getGetter()
|
||||
{
|
||||
return getter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for setter
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Method getSetter()
|
||||
{
|
||||
return setter;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for associatedType
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Class<? extends QRecordEntity> getAssociatedType()
|
||||
{
|
||||
return associatedType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for associationAnnotation
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QAssociation getAssociationAnnotation()
|
||||
{
|
||||
return associationAnnotation;
|
||||
}
|
||||
|
||||
}
|
@ -32,6 +32,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
*******************************************************************************/
|
||||
public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface>
|
||||
{
|
||||
public static final int DEFAULT_SORT_ORDER = 500;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Produce the metaData object. Generally, you don't want to add it to the instance
|
||||
@ -43,11 +46,13 @@ public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface>
|
||||
|
||||
/*******************************************************************************
|
||||
** In case this producer needs to run before (or after) others, this method
|
||||
** can help influence that (e.g., if used by MetaDataProducerHelper).
|
||||
** can control influence that (e.g., if used by MetaDataProducerHelper).
|
||||
**
|
||||
** Smaller values run first.
|
||||
*******************************************************************************/
|
||||
public int getSortOrder()
|
||||
{
|
||||
return (500);
|
||||
return (DEFAULT_SORT_ORDER);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,6 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
@ -38,17 +37,24 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
**
|
||||
*******************************************************************************/
|
||||
@JsonDeserialize(using = QBackendMetaDataDeserializer.class)
|
||||
public class QBackendMetaData
|
||||
public class QBackendMetaData implements TopLevelMetaDataInterface
|
||||
{
|
||||
private String name;
|
||||
private String backendType;
|
||||
|
||||
private Boolean usesVariants = false;
|
||||
private String variantOptionsTableName;
|
||||
|
||||
private Set<Capability> enabledCapabilities = new HashSet<>();
|
||||
private Set<Capability> disabledCapabilities = new HashSet<>();
|
||||
|
||||
private Boolean usesVariants = false;
|
||||
private String variantOptionsTableIdField;
|
||||
private String variantOptionsTableNameField;
|
||||
private String variantOptionsTableTypeField;
|
||||
private String variantOptionsTableTypeValue;
|
||||
private String variantOptionsTableUsernameField;
|
||||
private String variantOptionsTablePasswordField;
|
||||
private String variantOptionsTableApiKeyField;
|
||||
private String variantOptionsTableName;
|
||||
|
||||
// todo - at some point, we may want to apply this to secret properties on subclasses?
|
||||
// @JsonFilter("secretsFilter")
|
||||
|
||||
@ -59,6 +65,10 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData()
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// by default, we will turn off the query stats capability on all backends //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
withoutCapability(Capability.QUERY_STATS);
|
||||
}
|
||||
|
||||
|
||||
@ -199,6 +209,10 @@ public class QBackendMetaData
|
||||
public void setEnabledCapabilities(Set<Capability> enabledCapabilities)
|
||||
{
|
||||
this.enabledCapabilities = enabledCapabilities;
|
||||
if(this.disabledCapabilities != null)
|
||||
{
|
||||
this.disabledCapabilities.removeAll(enabledCapabilities);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -209,7 +223,7 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities)
|
||||
{
|
||||
this.enabledCapabilities = enabledCapabilities;
|
||||
setEnabledCapabilities(enabledCapabilities);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -221,7 +235,10 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withCapabilities(Set<Capability> enabledCapabilities)
|
||||
{
|
||||
this.enabledCapabilities = enabledCapabilities;
|
||||
for(Capability enabledCapability : enabledCapabilities)
|
||||
{
|
||||
withCapability(enabledCapability);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -238,6 +255,7 @@ public class QBackendMetaData
|
||||
this.enabledCapabilities = new HashSet<>();
|
||||
}
|
||||
this.enabledCapabilities.add(capability);
|
||||
this.disabledCapabilities.remove(capability);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -249,11 +267,10 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withCapabilities(Capability... enabledCapabilities)
|
||||
{
|
||||
if(this.enabledCapabilities == null)
|
||||
for(Capability enabledCapability : enabledCapabilities)
|
||||
{
|
||||
this.enabledCapabilities = new HashSet<>();
|
||||
withCapability(enabledCapability);
|
||||
}
|
||||
this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList());
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -277,6 +294,10 @@ public class QBackendMetaData
|
||||
public void setDisabledCapabilities(Set<Capability> disabledCapabilities)
|
||||
{
|
||||
this.disabledCapabilities = disabledCapabilities;
|
||||
if(this.enabledCapabilities != null)
|
||||
{
|
||||
this.enabledCapabilities.removeAll(disabledCapabilities);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -287,7 +308,7 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withDisabledCapabilities(Set<Capability> disabledCapabilities)
|
||||
{
|
||||
this.disabledCapabilities = disabledCapabilities;
|
||||
setDisabledCapabilities(disabledCapabilities);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -299,11 +320,10 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withoutCapabilities(Capability... disabledCapabilities)
|
||||
{
|
||||
if(this.disabledCapabilities == null)
|
||||
for(Capability disabledCapability : disabledCapabilities)
|
||||
{
|
||||
this.disabledCapabilities = new HashSet<>();
|
||||
withoutCapability(disabledCapability);
|
||||
}
|
||||
this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList());
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -315,7 +335,10 @@ public class QBackendMetaData
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withoutCapabilities(Set<Capability> disabledCapabilities)
|
||||
{
|
||||
this.disabledCapabilities = disabledCapabilities;
|
||||
for(Capability disabledCapability : disabledCapabilities)
|
||||
{
|
||||
withCapability(disabledCapability);
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -332,6 +355,7 @@ public class QBackendMetaData
|
||||
this.disabledCapabilities = new HashSet<>();
|
||||
}
|
||||
this.disabledCapabilities.add(capability);
|
||||
this.enabledCapabilities.remove(capability);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -381,7 +405,224 @@ public class QBackendMetaData
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantsOptionTableName
|
||||
** Getter for variantOptionsTableIdField
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableIdField()
|
||||
{
|
||||
return (this.variantOptionsTableIdField);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTableIdField
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableIdField(String variantOptionsTableIdField)
|
||||
{
|
||||
this.variantOptionsTableIdField = variantOptionsTableIdField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTableIdField
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTableIdField(String variantOptionsTableIdField)
|
||||
{
|
||||
this.variantOptionsTableIdField = variantOptionsTableIdField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTableNameField
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableNameField()
|
||||
{
|
||||
return (this.variantOptionsTableNameField);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTableNameField
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableNameField(String variantOptionsTableNameField)
|
||||
{
|
||||
this.variantOptionsTableNameField = variantOptionsTableNameField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTableNameField
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTableNameField(String variantOptionsTableNameField)
|
||||
{
|
||||
this.variantOptionsTableNameField = variantOptionsTableNameField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTableTypeField
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableTypeField()
|
||||
{
|
||||
return (this.variantOptionsTableTypeField);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTableTypeField
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableTypeField(String variantOptionsTableTypeField)
|
||||
{
|
||||
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTableTypeField
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTableTypeField(String variantOptionsTableTypeField)
|
||||
{
|
||||
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTableTypeValue
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableTypeValue()
|
||||
{
|
||||
return (this.variantOptionsTableTypeValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTableTypeValue
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
|
||||
{
|
||||
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTableTypeValue
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
|
||||
{
|
||||
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTableUsernameField
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableUsernameField()
|
||||
{
|
||||
return (this.variantOptionsTableUsernameField);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTableUsernameField
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
|
||||
{
|
||||
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTableUsernameField
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
|
||||
{
|
||||
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTablePasswordField
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTablePasswordField()
|
||||
{
|
||||
return (this.variantOptionsTablePasswordField);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTablePasswordField
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
|
||||
{
|
||||
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTablePasswordField
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
|
||||
{
|
||||
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTableApiKeyField
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableApiKeyField()
|
||||
{
|
||||
return (this.variantOptionsTableApiKeyField);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantOptionsTableApiKeyField
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
|
||||
{
|
||||
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantOptionsTableApiKeyField
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
|
||||
{
|
||||
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantOptionsTableName
|
||||
*******************************************************************************/
|
||||
public String getVariantOptionsTableName()
|
||||
{
|
||||
@ -391,7 +632,7 @@ public class QBackendMetaData
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for variantsOptionTableName
|
||||
** Setter for variantOptionsTableName
|
||||
*******************************************************************************/
|
||||
public void setVariantOptionsTableName(String variantOptionsTableName)
|
||||
{
|
||||
@ -401,12 +642,22 @@ public class QBackendMetaData
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for variantsOptionTableName
|
||||
** Fluent setter for variantOptionsTableName
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData withVariantsOptionTableName(String variantsOptionTableName)
|
||||
public QBackendMetaData withVariantOptionsTableName(String variantOptionsTableName)
|
||||
{
|
||||
this.variantOptionsTableName = variantsOptionTableName;
|
||||
this.variantOptionsTableName = variantOptionsTableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addSelfToInstance(QInstance qInstance)
|
||||
{
|
||||
qInstance.addBackend(this);
|
||||
}
|
||||
}
|
||||
|
@ -91,8 +91,9 @@ public class QInstance
|
||||
private Map<String, QQueueProviderMetaData> queueProviders = new LinkedHashMap<>();
|
||||
private Map<String, QQueueMetaData> queues = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, QMiddlewareInstanceMetaData> middlewareMetaData = new LinkedHashMap<>();
|
||||
private Map<String, QSupplementalInstanceMetaData> supplementalMetaData = new LinkedHashMap<>();
|
||||
|
||||
private String deploymentMode;
|
||||
private Map<String, String> environmentValues = new LinkedHashMap<>();
|
||||
private String defaultTimeZoneId = "UTC";
|
||||
|
||||
@ -1083,60 +1084,60 @@ public class QInstance
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for middlewareMetaData
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public Map<String, QMiddlewareInstanceMetaData> getMiddlewareMetaData()
|
||||
public Map<String, QSupplementalInstanceMetaData> getSupplementalMetaData()
|
||||
{
|
||||
return (this.middlewareMetaData);
|
||||
return (this.supplementalMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for middlewareMetaData
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QMiddlewareInstanceMetaData getMiddlewareMetaData(String type)
|
||||
public QSupplementalInstanceMetaData getSupplementalMetaData(String type)
|
||||
{
|
||||
if(this.middlewareMetaData == null)
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
return this.middlewareMetaData.get(type);
|
||||
return this.supplementalMetaData.get(type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for middlewareMetaData
|
||||
** Setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public void setMiddlewareMetaData(Map<String, QMiddlewareInstanceMetaData> middlewareMetaData)
|
||||
public void setSupplementalMetaData(Map<String, QSupplementalInstanceMetaData> supplementalMetaData)
|
||||
{
|
||||
this.middlewareMetaData = middlewareMetaData;
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for middlewareMetaData
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QInstance withMiddlewareMetaData(Map<String, QMiddlewareInstanceMetaData> middlewareMetaData)
|
||||
public QInstance withSupplementalMetaData(Map<String, QSupplementalInstanceMetaData> supplementalMetaData)
|
||||
{
|
||||
this.middlewareMetaData = middlewareMetaData;
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for middlewareMetaData
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QInstance withMiddlewareMetaData(QMiddlewareInstanceMetaData middlewareMetaData)
|
||||
public QInstance withSupplementalMetaData(QSupplementalInstanceMetaData supplementalMetaData)
|
||||
{
|
||||
if(this.middlewareMetaData == null)
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
this.middlewareMetaData = new HashMap<>();
|
||||
this.supplementalMetaData = new HashMap<>();
|
||||
}
|
||||
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
|
||||
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
|
||||
return (this);
|
||||
}
|
||||
|
||||
@ -1165,4 +1166,36 @@ public class QInstance
|
||||
}
|
||||
this.joinGraph = joinGraph;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for deploymentMode
|
||||
*******************************************************************************/
|
||||
public String getDeploymentMode()
|
||||
{
|
||||
return (this.deploymentMode);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for deploymentMode
|
||||
*******************************************************************************/
|
||||
public void setDeploymentMode(String deploymentMode)
|
||||
{
|
||||
this.deploymentMode = deploymentMode;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for deploymentMode
|
||||
*******************************************************************************/
|
||||
public QInstance withDeploymentMode(String deploymentMode)
|
||||
{
|
||||
this.deploymentMode = deploymentMode;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -27,42 +27,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base-class for instance-level meta-data defined for a specific middleware.
|
||||
** Base-class for instance-level meta-data defined by some supplemental module, etc,
|
||||
** outside of qqq core
|
||||
*******************************************************************************/
|
||||
public abstract class QMiddlewareInstanceMetaData
|
||||
public abstract class QSupplementalInstanceMetaData
|
||||
{
|
||||
protected String type;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for type
|
||||
*******************************************************************************/
|
||||
public String getType()
|
||||
{
|
||||
return (this.type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for type
|
||||
*******************************************************************************/
|
||||
public void setType(String type)
|
||||
{
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for type
|
||||
*******************************************************************************/
|
||||
public QMiddlewareInstanceMetaData withType(String type)
|
||||
{
|
||||
this.type = type;
|
||||
return (this);
|
||||
}
|
||||
public abstract String getType();
|
||||
|
||||
|
||||
|
@ -60,7 +60,6 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
|
||||
private String auth0ClientSecretField;
|
||||
private Serializable qqqRecordIdField;
|
||||
|
||||
|
||||
/////////////////////////////////////
|
||||
// fields on the accessToken table //
|
||||
/////////////////////////////////////
|
||||
|
@ -28,8 +28,8 @@ import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.templates.RenderTemplateAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
|
||||
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class QFieldMetaData implements Cloneable
|
||||
|
||||
private List<FieldAdornment> adornments;
|
||||
|
||||
private Map<String, QMiddlewareFieldMetaData> middlewareMetaData;
|
||||
private Map<String, QSupplementalFieldMetaData> supplementalMetaData;
|
||||
|
||||
|
||||
|
||||
@ -840,60 +840,60 @@ public class QFieldMetaData implements Cloneable
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for middlewareMetaData
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public Map<String, QMiddlewareFieldMetaData> getMiddlewareMetaData()
|
||||
public Map<String, QSupplementalFieldMetaData> getSupplementalMetaData()
|
||||
{
|
||||
return (this.middlewareMetaData);
|
||||
return (this.supplementalMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for middlewareMetaData
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QMiddlewareFieldMetaData getMiddlewareMetaData(String type)
|
||||
public QSupplementalFieldMetaData getSupplementalMetaData(String type)
|
||||
{
|
||||
if(this.middlewareMetaData == null)
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
return this.middlewareMetaData.get(type);
|
||||
return this.supplementalMetaData.get(type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for middlewareMetaData
|
||||
** Setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public void setMiddlewareMetaData(Map<String, QMiddlewareFieldMetaData> middlewareMetaData)
|
||||
public void setSupplementalMetaData(Map<String, QSupplementalFieldMetaData> supplementalMetaData)
|
||||
{
|
||||
this.middlewareMetaData = middlewareMetaData;
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for middlewareMetaData
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QFieldMetaData withMiddlewareMetaData(Map<String, QMiddlewareFieldMetaData> middlewareMetaData)
|
||||
public QFieldMetaData withSupplementalMetaData(Map<String, QSupplementalFieldMetaData> supplementalMetaData)
|
||||
{
|
||||
this.middlewareMetaData = middlewareMetaData;
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for middlewareMetaData
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QFieldMetaData withMiddlewareMetaData(QMiddlewareFieldMetaData middlewareMetaData)
|
||||
public QFieldMetaData withSupplementalMetaData(QSupplementalFieldMetaData supplementalMetaData)
|
||||
{
|
||||
if(this.middlewareMetaData == null)
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
this.middlewareMetaData = new HashMap<>();
|
||||
this.supplementalMetaData = new HashMap<>();
|
||||
}
|
||||
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
|
||||
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.fields;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base-class for field-level meta-data defined by some supplemental module, etc,
|
||||
** outside of qqq core
|
||||
*******************************************************************************/
|
||||
public abstract class QSupplementalFieldMetaData
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for type
|
||||
*******************************************************************************/
|
||||
public abstract String getType();
|
||||
|
||||
}
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
@ -38,14 +39,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
public class QFrontendFieldMetaData
|
||||
{
|
||||
private String name;
|
||||
private String label;
|
||||
private QFieldType type;
|
||||
private boolean isRequired;
|
||||
private boolean isEditable;
|
||||
private boolean isHeavy;
|
||||
private String possibleValueSourceName;
|
||||
private String displayFormat;
|
||||
private String name;
|
||||
private String label;
|
||||
private QFieldType type;
|
||||
private boolean isRequired;
|
||||
private boolean isEditable;
|
||||
private boolean isHeavy;
|
||||
private String possibleValueSourceName;
|
||||
private String displayFormat;
|
||||
private Serializable defaultValue;
|
||||
|
||||
private List<FieldAdornment> adornments;
|
||||
|
||||
@ -69,6 +71,7 @@ public class QFrontendFieldMetaData
|
||||
this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName();
|
||||
this.displayFormat = fieldMetaData.getDisplayFormat();
|
||||
this.adornments = fieldMetaData.getAdornments();
|
||||
this.defaultValue = fieldMetaData.getDefaultValue();
|
||||
}
|
||||
|
||||
|
||||
@ -170,4 +173,14 @@ public class QFrontendFieldMetaData
|
||||
return possibleValueSourceName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for defaultValue
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Serializable getDefaultValue()
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
@ -57,21 +59,22 @@ public class QFrontendTableMetaData
|
||||
private String label;
|
||||
private boolean isHidden;
|
||||
private String primaryKeyField;
|
||||
private String iconName;
|
||||
|
||||
private String iconName;
|
||||
|
||||
private Map<String, QFrontendFieldMetaData> fields;
|
||||
private List<QFieldSection> sections;
|
||||
|
||||
private List<QFrontendExposedJoin> exposedJoins;
|
||||
|
||||
private Set<String> capabilities;
|
||||
private Map<String, QFrontendFieldMetaData> fields;
|
||||
private List<QFieldSection> sections;
|
||||
private List<QFrontendExposedJoin> exposedJoins;
|
||||
private Map<String, QSupplementalTableMetaData> supplementalTableMetaData;
|
||||
private Set<String> capabilities;
|
||||
|
||||
private boolean readPermission;
|
||||
private boolean insertPermission;
|
||||
private boolean editPermission;
|
||||
private boolean deletePermission;
|
||||
|
||||
private boolean usesVariants;
|
||||
private String variantTableLabel;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// do not add setters. take values from the source-object in the constructor!! //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
@ -81,13 +84,13 @@ public class QFrontendTableMetaData
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields, boolean includeJoins)
|
||||
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins)
|
||||
{
|
||||
this.name = tableMetaData.getName();
|
||||
this.label = tableMetaData.getLabel();
|
||||
this.isHidden = tableMetaData.getIsHidden();
|
||||
|
||||
if(includeFields)
|
||||
if(includeFullMetaData)
|
||||
{
|
||||
this.primaryKeyField = tableMetaData.getPrimaryKeyField();
|
||||
this.fields = new HashMap<>();
|
||||
@ -116,7 +119,7 @@ public class QFrontendTableMetaData
|
||||
QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
|
||||
frontendExposedJoin.setLabel(exposedJoin.getLabel());
|
||||
frontendExposedJoin.setIsMany(exposedJoin.getIsMany());
|
||||
frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFields, false));
|
||||
frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFullMetaData, false));
|
||||
for(String joinName : exposedJoin.getJoinPath())
|
||||
{
|
||||
frontendExposedJoin.addJoin(qInstance.getJoin(joinName));
|
||||
@ -124,6 +127,28 @@ public class QFrontendTableMetaData
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// include supplemental meta data, based on if it's meant for full or partial frontend meta-data requests //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(tableMetaData.getSupplementalMetaData()).values())
|
||||
{
|
||||
boolean include;
|
||||
if(includeFullMetaData)
|
||||
{
|
||||
include = supplementalTableMetaData.includeInFullFrontendMetaData();
|
||||
}
|
||||
else
|
||||
{
|
||||
include = supplementalTableMetaData.includeInPartialFrontendMetaData();
|
||||
}
|
||||
|
||||
if(include)
|
||||
{
|
||||
this.supplementalTableMetaData = Objects.requireNonNullElseGet(this.supplementalTableMetaData, HashMap::new);
|
||||
this.supplementalTableMetaData.put(supplementalTableMetaData.getType(), supplementalTableMetaData);
|
||||
}
|
||||
}
|
||||
|
||||
if(tableMetaData.getIcon() != null)
|
||||
{
|
||||
this.iconName = tableMetaData.getIcon().getName();
|
||||
@ -135,6 +160,13 @@ public class QFrontendTableMetaData
|
||||
insertPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.INSERT);
|
||||
editPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.EDIT);
|
||||
deletePermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.DELETE);
|
||||
|
||||
QBackendMetaData backend = actionInput.getInstance().getBackend(tableMetaData.getBackendName());
|
||||
if(backend != null && backend.getUsesVariants())
|
||||
{
|
||||
usesVariants = true;
|
||||
variantTableLabel = actionInput.getInstance().getTable(backend.getVariantOptionsTableName()).getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -294,6 +326,17 @@ public class QFrontendTableMetaData
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for usesVariants
|
||||
**
|
||||
*******************************************************************************/
|
||||
public boolean getUsesVariants()
|
||||
{
|
||||
return usesVariants;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for exposedJoins
|
||||
**
|
||||
@ -302,4 +345,26 @@ public class QFrontendTableMetaData
|
||||
{
|
||||
return exposedJoins;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supplementalTableMetaData
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, QSupplementalTableMetaData> getSupplementalTableMetaData()
|
||||
{
|
||||
return supplementalTableMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for variantTableLabel
|
||||
*******************************************************************************/
|
||||
public String getVariantTableLabel()
|
||||
{
|
||||
return (this.variantTableLabel);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2023. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Version of a variant for a frontend to see
|
||||
*******************************************************************************/
|
||||
public class QFrontendVariant
|
||||
{
|
||||
private Serializable id;
|
||||
private String name;
|
||||
private String type;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for id
|
||||
*******************************************************************************/
|
||||
public Serializable getId()
|
||||
{
|
||||
return (this.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for id
|
||||
*******************************************************************************/
|
||||
public void setId(Serializable id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for id
|
||||
*******************************************************************************/
|
||||
public QFrontendVariant withId(Serializable id)
|
||||
{
|
||||
this.id = id;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for name
|
||||
*******************************************************************************/
|
||||
public String getName()
|
||||
{
|
||||
return (this.name);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for name
|
||||
*******************************************************************************/
|
||||
public void setName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for name
|
||||
*******************************************************************************/
|
||||
public QFrontendVariant withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for type
|
||||
*******************************************************************************/
|
||||
public String getType()
|
||||
{
|
||||
return (this.type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for type
|
||||
*******************************************************************************/
|
||||
public void setType(String type)
|
||||
{
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for type
|
||||
*******************************************************************************/
|
||||
public QFrontendVariant withType(String type)
|
||||
{
|
||||
this.type = type;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
@ -36,7 +38,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
** MetaData definition of an App - an entity that organizes tables & processes
|
||||
** and can be arranged hierarchically (e.g, apps can contain other apps).
|
||||
*******************************************************************************/
|
||||
public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRules
|
||||
public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface
|
||||
{
|
||||
private String name;
|
||||
private String label;
|
||||
@ -414,4 +416,14 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addSelfToInstance(QInstance qInstance)
|
||||
{
|
||||
qInstance.addApp(this);
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,21 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Create a new possible value source, for a table, with default settings.
|
||||
** e.g., name & table name from the tableName parameter; type=TABLE; and LABEL_ONLY format
|
||||
*******************************************************************************/
|
||||
public static QPossibleValueSource newForTable(String tableName)
|
||||
{
|
||||
return new QPossibleValueSource()
|
||||
.withName(tableName)
|
||||
.withType(QPossibleValueSourceType.TABLE)
|
||||
.withTableName(tableName)
|
||||
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -54,6 +54,9 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
|
||||
private BasepullConfiguration basepullConfiguration;
|
||||
private QPermissionRules permissionRules;
|
||||
|
||||
private Integer minInputRecords = null;
|
||||
private Integer maxInputRecords = null;
|
||||
|
||||
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
|
||||
private Map<String, QStepMetaData> steps; // this is the full map of possible steps
|
||||
|
||||
@ -61,6 +64,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
|
||||
|
||||
private QScheduleMetaData schedule;
|
||||
|
||||
private Map<String, QSupplementalProcessMetaData> supplementalMetaData;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -544,4 +549,126 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
|
||||
qInstance.addProcess(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public Map<String, QSupplementalProcessMetaData> getSupplementalMetaData()
|
||||
{
|
||||
return (this.supplementalMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QSupplementalProcessMetaData getSupplementalMetaData(String type)
|
||||
{
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
return (null);
|
||||
}
|
||||
return this.supplementalMetaData.get(type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public void setSupplementalMetaData(Map<String, QSupplementalProcessMetaData> supplementalMetaData)
|
||||
{
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QProcessMetaData withSupplementalMetaData(Map<String, QSupplementalProcessMetaData> supplementalMetaData)
|
||||
{
|
||||
this.supplementalMetaData = supplementalMetaData;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for supplementalMetaData
|
||||
*******************************************************************************/
|
||||
public QProcessMetaData withSupplementalMetaData(QSupplementalProcessMetaData supplementalMetaData)
|
||||
{
|
||||
if(this.supplementalMetaData == null)
|
||||
{
|
||||
this.supplementalMetaData = new HashMap<>();
|
||||
}
|
||||
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for minInputRecords
|
||||
*******************************************************************************/
|
||||
public Integer getMinInputRecords()
|
||||
{
|
||||
return (this.minInputRecords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for minInputRecords
|
||||
*******************************************************************************/
|
||||
public void setMinInputRecords(Integer minInputRecords)
|
||||
{
|
||||
this.minInputRecords = minInputRecords;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for minInputRecords
|
||||
*******************************************************************************/
|
||||
public QProcessMetaData withMinInputRecords(Integer minInputRecords)
|
||||
{
|
||||
this.minInputRecords = minInputRecords;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxInputRecords
|
||||
*******************************************************************************/
|
||||
public Integer getMaxInputRecords()
|
||||
{
|
||||
return (this.maxInputRecords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for maxInputRecords
|
||||
*******************************************************************************/
|
||||
public void setMaxInputRecords(Integer maxInputRecords)
|
||||
{
|
||||
this.maxInputRecords = maxInputRecords;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for maxInputRecords
|
||||
*******************************************************************************/
|
||||
public QProcessMetaData withMaxInputRecords(Integer maxInputRecords)
|
||||
{
|
||||
this.maxInputRecords = maxInputRecords;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user