mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-21 06:28:44 +00:00
Compare commits
110 Commits
version-0.
...
version-0.
Author | SHA1 | Date | |
---|---|---|---|
a7faf52817 | |||
5a9a0754a6 | |||
4790f55243 | |||
f0450ef621 | |||
ed5839aa0a | |||
56f05c74fc | |||
4f8f8bad9a | |||
4a1853faa5 | |||
c972f9cecc | |||
efa796bb39 | |||
a05e74a7d0 | |||
41fcb09c70 | |||
6b12390f6c | |||
9889320fa6 | |||
c00120a1fc | |||
029f436071 | |||
f1ff53059f | |||
18a8656ba2 | |||
e75b645639 | |||
804efbd608 | |||
fe30d2654b | |||
97b803a86a | |||
2daa42aeb1 | |||
4344fa472a | |||
6bb810c1f4 | |||
d65af53090 | |||
ac6a7ba15a | |||
1c150e207a | |||
f9408716ac | |||
396f02265b | |||
a1674792c6 | |||
22565b3ecd | |||
d2e7b794f4 | |||
7000da409a | |||
3f0e09e32a | |||
70d9d259c1 | |||
4dc9c52ee0 | |||
964405d210 | |||
369ba3c8d7 | |||
24d5406ee3 | |||
984012f3a3 | |||
e8264d915f | |||
46adfa8e24 | |||
178078282c | |||
d03e947889 | |||
43de1cf749 | |||
d3fa1df56f | |||
69a6104393 | |||
e831c75e1e | |||
eba9b0b4af | |||
9f82f35bcf | |||
ff07490f94 | |||
680696491f | |||
a35e30059c | |||
a37d22b0d0 | |||
9e02476ee7 | |||
2b0974f4a5 | |||
9a58c7683b | |||
a556bf9764 | |||
41877a7055 | |||
c07a77d4a6 | |||
23e9abeb74 | |||
e4d37e3db9 | |||
4b31a8b4bb | |||
d7e7315dc8 | |||
de05e4ae58 | |||
150582964b | |||
2874b98b66 | |||
7fae3e2329 | |||
428f48602b | |||
799b695e14 | |||
19d88910b5 | |||
2ad4b22f55 | |||
040dae55d5 | |||
dd9253fde4 | |||
8a4d5bfb34 | |||
e1c53b9d48 | |||
a6656af040 | |||
0e68bf1e72 | |||
1b672afcd0 | |||
5005c38c18 | |||
1cdf437551 | |||
04cddb8d5d | |||
0d6bd013f5 | |||
7b6284c06f | |||
edcc27485e | |||
280373ddc5 | |||
25815ebc25 | |||
59cbf83860 | |||
6186b17e92 | |||
30003b729c | |||
293b3e4207 | |||
470321dcf6 | |||
48cfdeffa1 | |||
14c7fbe370 | |||
8454f94020 | |||
697261f91b | |||
61d493a4f5 | |||
c21c89e85f | |||
a9a3e3b19e | |||
5cf9e7b60d | |||
9b34ee7fe7 | |||
241741e2e5 | |||
a769d8942c | |||
c22fc89cbb | |||
060da69afb | |||
3691ad87e5 | |||
583ac70563 | |||
28e0bdadbf | |||
40afb629e7 |
@ -262,6 +262,7 @@
|
||||
<module name="Header">
|
||||
<property name="headerFile" value="checkstyle/license.txt"/>
|
||||
<property name="fileExtensions" value="java"/>
|
||||
<property name="ignoreLines" value="3"/>
|
||||
</module>
|
||||
<module name="SuppressWarningsFilter"/>
|
||||
</module>
|
||||
|
3
pom.xml
3
pom.xml
@ -39,10 +39,11 @@
|
||||
<module>qqq-middleware-lambda</module>
|
||||
<module>qqq-utility-lambdas</module>
|
||||
<module>qqq-sample-project</module>
|
||||
<module>qqq-middleware-slack</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<revision>0.8.0</revision>
|
||||
<revision>0.11.0</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
|
@ -56,6 +56,12 @@
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>quicksight</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-secretsmanager</artifactId>
|
||||
<version>1.12.385</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
@ -69,7 +75,7 @@
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20210307</version>
|
||||
<version>20220320</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
@ -167,6 +173,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>4.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.actions;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
@ -50,11 +52,13 @@ public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O
|
||||
*******************************************************************************/
|
||||
public Future<Void> executeAsync(I input, O output)
|
||||
{
|
||||
CapturedContext capturedContext = QContext.capture();
|
||||
CompletableFuture<Void> completableFuture = new CompletableFuture<>();
|
||||
Executors.newCachedThreadPool().submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
QContext.init(capturedContext);
|
||||
execute(input, output);
|
||||
completableFuture.complete(null);
|
||||
}
|
||||
@ -62,6 +66,10 @@ public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O
|
||||
{
|
||||
completableFuture.completeExceptionally(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
});
|
||||
return (completableFuture);
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.actions;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
@ -50,11 +52,13 @@ public abstract class AbstractQActionFunction<I extends AbstractActionInput, O e
|
||||
*******************************************************************************/
|
||||
public Future<O> executeAsync(I input)
|
||||
{
|
||||
CapturedContext capturedContext = QContext.capture();
|
||||
CompletableFuture<O> completableFuture = new CompletableFuture<>();
|
||||
Executors.newCachedThreadPool().submit(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
QContext.init(capturedContext);
|
||||
O output = execute(input);
|
||||
completableFuture.complete(output);
|
||||
}
|
||||
@ -62,6 +66,10 @@ public abstract class AbstractQActionFunction<I extends AbstractActionInput, O e
|
||||
{
|
||||
completableFuture.completeExceptionally(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
});
|
||||
return (completableFuture);
|
||||
}
|
||||
|
@ -25,9 +25,12 @@ package com.kingsrook.qqq.backend.core.actions;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
|
||||
|
||||
@ -43,9 +46,22 @@ public class ActionHelper
|
||||
*******************************************************************************/
|
||||
public static void validateSession(AbstractActionInput request) throws QException
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QSession qSession = QContext.getQSession();
|
||||
|
||||
if(qInstance == null)
|
||||
{
|
||||
throw (new QException("QInstance was not set in QContext."));
|
||||
}
|
||||
|
||||
if(qSession == null)
|
||||
{
|
||||
throw (new QException("QSession was not set in QContext."));
|
||||
}
|
||||
|
||||
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
|
||||
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
|
||||
if(!authenticationModule.isSessionValid(request.getInstance(), request.getSession()))
|
||||
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
|
||||
if(!authenticationModule.isSessionValid(qInstance, qSession))
|
||||
{
|
||||
throw new QAuthenticationException("Invalid session in request");
|
||||
}
|
||||
|
@ -30,13 +30,14 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import com.kingsrook.qqq.backend.core.context.CapturedContext;
|
||||
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.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 org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -44,7 +45,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class AsyncJobManager
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(AsyncJobManager.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
|
||||
|
||||
|
||||
|
||||
@ -72,8 +73,10 @@ public class AsyncJobManager
|
||||
|
||||
try
|
||||
{
|
||||
CapturedContext capturedContext = QContext.capture();
|
||||
CompletableFuture<T> future = CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
QContext.init(capturedContext);
|
||||
return (runAsyncJob(jobName, asyncJob, uuidAndTypeStateKey, asyncJobStatus));
|
||||
});
|
||||
|
||||
@ -153,6 +156,7 @@ public class AsyncJobManager
|
||||
finally
|
||||
{
|
||||
Thread.currentThread().setName(originalThreadName);
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,9 +27,10 @@ import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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;
|
||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -40,7 +41,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class AsyncRecordPipeLoop
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(AsyncRecordPipeLoop.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(AsyncRecordPipeLoop.class);
|
||||
|
||||
private static final int TIMEOUT_AFTER_NO_RECORDS_MS = 10 * 60 * 1000;
|
||||
|
||||
@ -62,7 +63,7 @@ public class AsyncRecordPipeLoop
|
||||
** @param consumer lambda that consumes records from the pipe
|
||||
* e.g., a transform/load step.
|
||||
*******************************************************************************/
|
||||
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable> supplier, UnsafeSupplier<Integer> consumer) throws QException
|
||||
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable, QException> supplier, UnsafeSupplier<Integer, QException> consumer) throws QException
|
||||
{
|
||||
///////////////////////////////////////////////////
|
||||
// start the extraction function as an async job //
|
||||
@ -175,32 +176,4 @@ public class AsyncRecordPipeLoop
|
||||
return (recordCount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
public interface UnsafeFunction<T, R>
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
R apply(T t) throws QException;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@FunctionalInterface
|
||||
public interface UnsafeSupplier<T>
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
T get() throws QException;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,227 @@
|
||||
/*
|
||||
* 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.audits;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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;
|
||||
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.audits.AuditInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditOutput;
|
||||
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.data.QRecord;
|
||||
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.QUser;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Insert an audit (e.g., the same one) against 1 or more records.
|
||||
**
|
||||
** Takes care of managing the foreign key tables.
|
||||
**
|
||||
** Enforces that security key values are provided, if the table has any.
|
||||
*******************************************************************************/
|
||||
public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput>
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AuditAction.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void execute(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
|
||||
{
|
||||
new AuditAction().execute(new AuditInput()
|
||||
.withAuditTableName(tableName)
|
||||
.withRecordIdList(List.of(recordId))
|
||||
.withSecurityKeyValues(securityKeyValues)
|
||||
.withMessage(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public AuditOutput execute(AuditInput input)
|
||||
{
|
||||
AuditOutput auditOutput = new AuditOutput();
|
||||
try
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(input.getAuditTableName());
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QException("Requested audit for an unrecognized table name: " + input.getAuditTableName()));
|
||||
}
|
||||
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
if(input.getSecurityKeyValues() == null || !input.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
|
||||
{
|
||||
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + input.getAuditTableName()));
|
||||
}
|
||||
}
|
||||
|
||||
Integer auditTableId = getIdForName("auditTable", input.getAuditTableName());
|
||||
Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(input.getAuditUserName(), getSessionUserName()));
|
||||
Instant timestamp = Objects.requireNonNullElse(input.getTimestamp(), Instant.now());
|
||||
|
||||
List<QRecord> auditRecords = new ArrayList<>();
|
||||
for(Integer recordId : input.getRecordIdList())
|
||||
{
|
||||
QRecord record = new QRecord()
|
||||
.withValue("auditTableId", auditTableId)
|
||||
.withValue("auditUserId", auditUserId)
|
||||
.withValue("timestamp", timestamp)
|
||||
.withValue("message", input.getMessage())
|
||||
.withValue("recordId", recordId);
|
||||
|
||||
if(input.getSecurityKeyValues() != null)
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : input.getSecurityKeyValues().entrySet())
|
||||
{
|
||||
record.setValue(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
auditRecords.add(record);
|
||||
}
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("audit");
|
||||
insertInput.setRecords(auditRecords);
|
||||
new InsertAction().execute(insertInput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error performing an audit", e);
|
||||
}
|
||||
return (auditOutput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String getSessionUserName()
|
||||
{
|
||||
QUser user = QContext.getQSession().getUser();
|
||||
if(user == null)
|
||||
{
|
||||
return ("Unknown");
|
||||
}
|
||||
return (user.getFullName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Integer getIdForName(String tableName, String nameValue) throws QException
|
||||
{
|
||||
Integer id = fetchIdFromName(tableName, nameValue);
|
||||
if(id != null)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LOG.debug("Inserting " + tableName + " named " + nameValue);
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(tableName);
|
||||
QRecord record = new QRecord().withValue("name", nameValue);
|
||||
|
||||
if(tableName.equals("auditTable"))
|
||||
{
|
||||
QTableMetaData table = QContext.getQInstance().getTable(nameValue);
|
||||
if(table != null)
|
||||
{
|
||||
record.setValue("label", table.getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
insertInput.setRecords(List.of(record));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
id = insertOutput.getRecords().get(0).getValueInteger("id");
|
||||
if(id != null)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// assume this may mean a dupe-key - so - try another fetch below //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
LOG.debug("Caught error inserting " + tableName + " named " + nameValue + " - will try to re-fetch", e);
|
||||
}
|
||||
|
||||
id = fetchIdFromName(tableName, nameValue);
|
||||
if(id != null)
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
/////////////
|
||||
// give up //
|
||||
/////////////
|
||||
throw (new QException("Unable to get id for " + tableName + " named " + nameValue));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Integer fetchIdFromName(String tableName, String nameValue) throws QException
|
||||
{
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(tableName);
|
||||
getInput.setUniqueKey(Map.of("name", nameValue));
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
if(getOutput.getRecord() != null)
|
||||
{
|
||||
return (getOutput.getRecord().getValueInteger("id"));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -27,6 +27,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||
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.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
@ -37,7 +38,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAuto
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.QLogger;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@ public class RecordAutomationStatusUpdater
|
||||
String className = stackTraceElement.getClassName();
|
||||
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test"))
|
||||
{
|
||||
LOG.debug(session, "Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
|
||||
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
@ -92,6 +92,12 @@ public class RecordAutomationStatusUpdater
|
||||
{
|
||||
for(QRecord record : records)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd //
|
||||
// lose that pending status, which would be a Bad Thing™... //
|
||||
// problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
|
||||
// todo - another field - for the automation timestamp??
|
||||
}
|
||||
@ -137,8 +143,7 @@ public class RecordAutomationStatusUpdater
|
||||
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus);
|
||||
if(didSetStatusField)
|
||||
{
|
||||
UpdateInput updateInput = new UpdateInput(instance);
|
||||
updateInput.setSession(session);
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(table.getName());
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -39,7 +39,9 @@ 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.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.query.QCriteriaOperator;
|
||||
@ -52,15 +54,13 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.scheduler.StandardScheduledExecutor;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -74,7 +74,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class PollingAutomationPerTableRunner implements Runnable
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(PollingAutomationPerTableRunner.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(PollingAutomationPerTableRunner.class);
|
||||
|
||||
private final TableActions tableActions;
|
||||
private final String name;
|
||||
@ -174,9 +174,11 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
QContext.init(instance, sessionSupplier.get());
|
||||
|
||||
String originalThreadName = Thread.currentThread().getName();
|
||||
Thread.currentThread().setName(name + StandardScheduledExecutor.newThreadNameRandomSuffix());
|
||||
LOG.debug("Running " + this.getClass().getSimpleName() + "[" + name + "]");
|
||||
Thread.currentThread().setName(name);
|
||||
LOG.info("Running " + this.getClass().getSimpleName() + "[" + name + "]");
|
||||
|
||||
try
|
||||
{
|
||||
@ -190,6 +192,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
finally
|
||||
{
|
||||
Thread.currentThread().setName(originalThreadName);
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -198,30 +201,33 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
/*******************************************************************************
|
||||
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
|
||||
*******************************************************************************/
|
||||
private void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus, List<TableAutomationAction> actions) throws QException
|
||||
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus, List<TableAutomationAction> actions) throws QException
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(actions))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug(" Query for records " + automationStatus + " in " + table);
|
||||
LOG.info(" Query for records " + automationStatus + " in " + table);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// run an async-pipe loop - that will query for records in PENDING - put them in a pipe - then apply actions to them //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
RecordPipe recordPipe = new RecordPipe();
|
||||
AsyncRecordPipeLoop asyncRecordPipeLoop = new AsyncRecordPipeLoop();
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
AsyncRecordPipeLoop asyncRecordPipeLoop = new AsyncRecordPipeLoop();
|
||||
|
||||
RecordPipe recordPipe = automationDetails.getOverrideBatchSize() == null
|
||||
? new RecordPipe() : new RecordPipe(automationDetails.getOverrideBatchSize());
|
||||
|
||||
asyncRecordPipeLoop.run("PollingAutomationRunner>Query>" + automationStatus + ">" + table.getName(), null, recordPipe, (status) ->
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(instance);
|
||||
queryInput.setSession(session);
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(table.getName());
|
||||
|
||||
AutomationStatusTrackingType statusTrackingType = table.getAutomationDetails().getStatusTracking().getType();
|
||||
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
|
||||
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType))
|
||||
{
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(table.getAutomationDetails().getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -270,11 +276,11 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(session, 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);
|
||||
LOG.info("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(session, table, matchingQRecords, action);
|
||||
LOG.info(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
|
||||
applyActionToMatchingRecords(instance, session, table, matchingQRecords, action);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
@ -313,8 +319,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
*******************************************************************************/
|
||||
private List<QRecord> getRecordsMatchingActionFilter(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(instance);
|
||||
queryInput.setSession(session);
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(table.getName());
|
||||
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
@ -352,8 +357,9 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
|
||||
/*******************************************************************************
|
||||
** Finally, actually run action code against a list of known matching records.
|
||||
** todo not commit - move to somewhere genericer
|
||||
*******************************************************************************/
|
||||
private void applyActionToMatchingRecords(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
|
||||
public static void applyActionToMatchingRecords(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
|
||||
{
|
||||
if(StringUtils.hasContent(action.getProcessName()))
|
||||
{
|
||||
@ -362,8 +368,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
// tell it to SKIP frontend steps. //
|
||||
// give the process a callback w/ a query filter that has the p-keys of these records. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
RunProcessInput runProcessInput = new RunProcessInput(instance);
|
||||
runProcessInput.setSession(session);
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
runProcessInput.setProcessName(action.getProcessName());
|
||||
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
|
||||
runProcessInput.setCallback(new QProcessCallback()
|
||||
@ -386,8 +391,7 @@ public class PollingAutomationPerTableRunner implements Runnable
|
||||
else if(action.getCodeReference() != null)
|
||||
{
|
||||
LOG.debug(" Executing action: [" + action.getName() + "] as code reference: " + action.getCodeReference());
|
||||
RecordAutomationInput input = new RecordAutomationInput(instance);
|
||||
input.setSession(session);
|
||||
RecordAutomationInput input = new RecordAutomationInput();
|
||||
input.setTableName(table.getName());
|
||||
input.setRecordList(records);
|
||||
|
||||
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPostInsertCustomizer
|
||||
{
|
||||
protected InsertInput insertInput;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract List<QRecord> apply(List<QRecord> records);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for insertInput
|
||||
**
|
||||
*******************************************************************************/
|
||||
public InsertInput getInsertInput()
|
||||
{
|
||||
return insertInput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for insertInput
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setInsertInput(InsertInput insertInput)
|
||||
{
|
||||
this.insertInput = insertInput;
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.customizers;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractPostQueryCustomizer
|
||||
{
|
||||
protected AbstractTableActionInput input;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract List<QRecord> apply(List<QRecord> records);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for input
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AbstractTableActionInput getInput()
|
||||
{
|
||||
return (input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for input
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setInput(AbstractTableActionInput input)
|
||||
{
|
||||
this.input = input;
|
||||
}
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.customizers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
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.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;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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".
|
||||
**
|
||||
** 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract QRecord buildChildForRecord(QRecord parentRecord) throws QException;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract String getChildTableName();
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract String getForeignKeyFieldName();
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract RelationshipType getRelationshipType();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public List<QRecord> apply(List<QRecord> records)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<QRecord> rs = new ArrayList<>();
|
||||
List<QRecord> childrenToInsert = new ArrayList<>();
|
||||
QTableMetaData table = getInsertInput().getTable();
|
||||
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// 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)
|
||||
{
|
||||
if(record.getValue(getForeignKeyFieldName()) == null)
|
||||
{
|
||||
childrenToInsert.add(buildChildForRecord(record));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are no children to insert, then just return the original record list //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
if(childrenToInsert.isEmpty())
|
||||
{
|
||||
return (records);
|
||||
}
|
||||
|
||||
/////////////////////////
|
||||
// insert the children //
|
||||
/////////////////////////
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(getChildTableName());
|
||||
insertInput.setRecords(childrenToInsert);
|
||||
insertInput.setTransaction(this.insertInput.getTransaction());
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
Iterator<QRecord> insertedRecordIterator = insertOutput.getRecords().iterator();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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)
|
||||
{
|
||||
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
|
||||
if(record.getValue(getForeignKeyFieldName()) == null)
|
||||
{
|
||||
Serializable foreignKey = insertedRecordIterator.next().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);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// 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(Exception e)
|
||||
{
|
||||
throw new RuntimeException("Error inserting new child records for new parent records", e);
|
||||
}
|
||||
}
|
||||
}
|
@ -28,13 +28,12 @@ import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -42,7 +41,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class QCodeLoader
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(QCodeLoader.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QCodeLoader.class);
|
||||
|
||||
|
||||
|
||||
@ -61,6 +60,21 @@ public class QCodeLoader
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static <T> Optional<T> getTableCustomizer(Class<T> expectedClass, QTableMetaData table, String customizerName)
|
||||
{
|
||||
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
|
||||
if(codeReference.isPresent())
|
||||
{
|
||||
return (Optional.ofNullable(QCodeLoader.getAdHoc(expectedClass, codeReference.get())));
|
||||
}
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -49,6 +49,18 @@ public class TableCustomizer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TableCustomizer(String role, Class<?> expectedType)
|
||||
{
|
||||
this.role = role;
|
||||
this.expectedType = expectedType;
|
||||
this.validationFunction = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for role
|
||||
**
|
||||
|
@ -22,10 +22,6 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.customizers;
|
||||
|
||||
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Enum definition of possible table customizers - "roles" for custom code that
|
||||
** can be applied to tables.
|
||||
@ -43,12 +39,8 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
*******************************************************************************/
|
||||
public enum TableCustomizers
|
||||
{
|
||||
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) ->
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Function<QRecord, QRecord> function = (Function<QRecord, QRecord>) x;
|
||||
QRecord output = function.apply(new QRecord());
|
||||
})));
|
||||
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", AbstractPostQueryCustomizer.class)),
|
||||
POST_INSERT_RECORD(new TableCustomizer("postInsertRecord", AbstractPostInsertCustomizer.class));
|
||||
|
||||
|
||||
private final TableCustomizer tableCustomizer;
|
||||
|
@ -25,13 +25,21 @@ package com.kingsrook.qqq.backend.core.actions.dashboard;
|
||||
import java.io.Serializable;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -48,7 +56,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
protected String openTopLevelBulletList()
|
||||
{
|
||||
return ("""
|
||||
<div style="padding: 1rem;">
|
||||
<div style="padding-left: 2rem;">
|
||||
<ul>""");
|
||||
}
|
||||
|
||||
@ -101,7 +109,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
*******************************************************************************/
|
||||
protected String bulletNameValue(String name, String value)
|
||||
{
|
||||
return ("<li><b>" + name + "</b> " + value + "</li>");
|
||||
return ("<li><b>" + name + "</b> " + Objects.requireNonNullElse(value, "--") + "</li>");
|
||||
}
|
||||
|
||||
|
||||
@ -117,6 +125,38 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkTableBulkLoadChildren(String tableName) throws QException
|
||||
{
|
||||
return ("#/launchProcess=" + tableName + ".bulkInsert");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkTableCreate(RenderWidgetInput input, String tableName) throws QException
|
||||
{
|
||||
String tablePath = input.getInstance().getTablePath(input, tableName);
|
||||
return (tablePath + "/create");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
|
||||
{
|
||||
String tablePath = input.getInstance().getTablePath(input, tableName);
|
||||
return (tablePath + "/create?defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -128,6 +168,27 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException
|
||||
{
|
||||
String href = linkTableFilter(input, tableName, filter);
|
||||
return ("<a href=\"" + href + "\">" + QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + " " + StringUtils.plural(noOfRecords, singularLabel, pluralLabel) + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String aHrefViewRecord(RenderWidgetInput input, String tableName, Serializable id, String linkText) throws QException
|
||||
{
|
||||
return ("<a href=\"" + linkRecordView(input, tableName, id) + "\">" + linkText + "</a>");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -139,6 +200,17 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkRecordView(AbstractActionInput input, String tableName, Serializable recordId) throws QException
|
||||
{
|
||||
String tablePath = input.getInstance().getTablePath(input, tableName);
|
||||
return (tablePath + "/" + recordId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -150,4 +222,48 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
|
||||
return (tablePath + "/" + recordId + "/" + processName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkTableCreateChild(String childTableName, Map<String, Serializable> defaultValues)
|
||||
{
|
||||
return (linkTableCreateChild(childTableName, defaultValues, defaultValues.keySet()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String aHrefTableCreateChild(String childTableName, Map<String, Serializable> defaultValues)
|
||||
{
|
||||
return (aHrefTableCreateChild(childTableName, defaultValues, defaultValues.keySet()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String linkTableCreateChild(String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields)
|
||||
{
|
||||
Map<String, Integer> disabledFieldsMap = disabledFields.stream().collect(Collectors.toMap(k -> k, k -> 1));
|
||||
|
||||
return ("#/createChild=" + childTableName
|
||||
+ "/defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), StandardCharsets.UTF_8).replaceAll("\\+", "%20")
|
||||
+ "/disabledFields=" + URLEncoder.encode(JsonUtils.toJson(disabledFieldsMap), StandardCharsets.UTF_8).replaceAll("\\+", "%20"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String aHrefTableCreateChild(String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields)
|
||||
{
|
||||
return ("<a href=\"" + linkTableCreateChild(childTableName, defaultValues, defaultValues.keySet()) + "\">Create new</a>");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRe
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
@ -53,12 +52,9 @@ public class RenderWidgetAction
|
||||
///////////////////////////////////////////////////////////////
|
||||
// move default values from meta data into this render input //
|
||||
///////////////////////////////////////////////////////////////
|
||||
if(input.getWidgetMetaData() instanceof QWidgetMetaData widgetMetaData)
|
||||
for(Map.Entry<String, Serializable> entry : input.getWidgetMetaData().getDefaultValues().entrySet())
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : widgetMetaData.getDefaultValues().entrySet())
|
||||
{
|
||||
input.addQueryParam(entry.getKey(), ValueUtils.getValueAsString(entry.getValue()));
|
||||
}
|
||||
input.addQueryParam(entry.getKey(), ValueUtils.getValueAsString(entry.getValue()));
|
||||
}
|
||||
|
||||
return (widgetRenderer.render(input));
|
||||
|
@ -24,10 +24,29 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
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.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -46,4 +65,102 @@ public abstract class AbstractWidgetRenderer
|
||||
*******************************************************************************/
|
||||
public abstract RenderWidgetOutput render(RenderWidgetInput input) throws QException;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected boolean setupDropdowns(RenderWidgetInput input, QWidgetMetaData metaData, QWidgetData widgetData) throws QException
|
||||
{
|
||||
List<List<Map<String, String>>> pvsData = new ArrayList<>();
|
||||
List<String> pvsLabels = new ArrayList<>();
|
||||
List<String> pvsNames = new ArrayList<>();
|
||||
List<String> missingRequiredSelections = new ArrayList<>();
|
||||
for(WidgetDropdownData dropdownData : CollectionUtils.nonNullList(metaData.getDropdowns()))
|
||||
{
|
||||
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
|
||||
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
|
||||
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
|
||||
pvsLabels.add(pvsLabel);
|
||||
pvsNames.add(possibleValueSourceName);
|
||||
|
||||
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
|
||||
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
|
||||
|
||||
if(dropdownData.getForeignKeyFieldName() != null)
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// look for an id in the query params //
|
||||
////////////////////////////////////////
|
||||
Integer id = null;
|
||||
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
|
||||
{
|
||||
id = Integer.parseInt(input.getQueryParams().get("id"));
|
||||
}
|
||||
if(id != null)
|
||||
{
|
||||
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
|
||||
new QFilterCriteria(
|
||||
dropdownData.getForeignKeyFieldName(),
|
||||
QCriteriaOperator.EQUALS,
|
||||
id)));
|
||||
}
|
||||
}
|
||||
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
|
||||
|
||||
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
|
||||
pvsData.add(dropdownOptionList);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// sort results, dedupe, and add to map //
|
||||
//////////////////////////////////////////
|
||||
Set<String> exists = new HashSet<>();
|
||||
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
|
||||
output.getResults().sort(Comparator.comparing(QPossibleValue::getLabel));
|
||||
for(QPossibleValue<?> possibleValue : output.getResults())
|
||||
{
|
||||
dropdownOptionList.add(Map.of(
|
||||
"id", String.valueOf(possibleValue.getId()),
|
||||
"label", possibleValue.getLabel()
|
||||
));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// because we know the dropdowns and what the field names will be when something is selected, we can make //
|
||||
// sure that something has been selected, and if not, display a message that a selection needs made //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(dropdownData.getIsRequired())
|
||||
{
|
||||
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
|
||||
{
|
||||
missingRequiredSelections.add(pvsLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widgetData.setDropdownNameList(pvsNames);
|
||||
widgetData.setDropdownLabelList(pvsLabels);
|
||||
widgetData.setDropdownDataList(pvsData);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// if there are any missing required dropdowns, build up a message to display //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(missingRequiredSelections.size() > 0)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder("Please select a ").append(StringUtils.joinWithCommasAndAnd(missingRequiredSelections));
|
||||
sb.append(" from the ").append(StringUtils.plural(missingRequiredSelections.size(), "dropdown", "dropdowns")).append(" above.");
|
||||
widgetData.setDropdownNeedsSelectedText(sb.toString());
|
||||
return (false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,9 +22,14 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -42,11 +47,14 @@ import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListDat
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.AbstractWidgetMetaDataBuilder;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -58,13 +66,76 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QWidgetMetaData defineWidgetFromJoin(QJoinMetaData join)
|
||||
public static Builder widgetMetaDataBuilder(QJoinMetaData join)
|
||||
{
|
||||
return (new QWidgetMetaData()
|
||||
return (new Builder(new QWidgetMetaData()
|
||||
.withName(join.getName())
|
||||
.withIsCard(true)
|
||||
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null))
|
||||
.withType(WidgetType.CHILD_RECORD_LIST.getType())
|
||||
.withDefaultValue("joinName", join.getName()));
|
||||
.withDefaultValue("joinName", join.getName())));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class Builder extends AbstractWidgetMetaDataBuilder
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder(QWidgetMetaData widgetMetaData)
|
||||
{
|
||||
super(widgetMetaData);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withName(String name)
|
||||
{
|
||||
widgetMetaData.setName(name);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withLabel(String label)
|
||||
{
|
||||
widgetMetaData.setLabel(label);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withCanAddChildRecord(boolean b)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("canAddChildRecord", true);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Builder withDisabledFieldsForNewChildRecords(Set<String> disabledFieldsForNewChildRecords)
|
||||
{
|
||||
widgetMetaData.withDefaultValue("disabledFieldsForNewChildRecords", new HashSet<>(disabledFieldsForNewChildRecords));
|
||||
return (this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -84,8 +155,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
// fetch the record that we're getting children for. //
|
||||
// e.g., the left-side of the join, with the input id //
|
||||
////////////////////////////////////////////////////////
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(join.getLeftTable());
|
||||
getInput.setPrimaryKey(id);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
@ -107,8 +177,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
}
|
||||
filter.setOrderBys(join.getOrderBys());
|
||||
|
||||
QueryInput queryInput = new QueryInput(input.getInstance());
|
||||
queryInput.setSession(input.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getRightTable());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
@ -117,9 +186,32 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
|
||||
|
||||
QTableMetaData table = input.getInstance().getTable(join.getRightTable());
|
||||
String tablePath = input.getInstance().getTablePath(input, table.getName());
|
||||
String viewAllLink = tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset());
|
||||
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
|
||||
|
||||
return (new RenderWidgetOutput(new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink)));
|
||||
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink);
|
||||
|
||||
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
|
||||
{
|
||||
widgetData.setCanAddChildRecord(true);
|
||||
|
||||
//////////////////////////////////////////////////////////
|
||||
// new child records must have values from the join-ons //
|
||||
//////////////////////////////////////////////////////////
|
||||
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
{
|
||||
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
|
||||
}
|
||||
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
|
||||
|
||||
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
|
||||
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
|
||||
{
|
||||
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
|
||||
}
|
||||
}
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.dashboard.widgets;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class DefaultWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
return new RenderWidgetOutput(new DefaultWidgetData(input));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static class DefaultWidgetData extends QWidgetData
|
||||
{
|
||||
private final String type;
|
||||
private final Map<String, String> queryParams;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public DefaultWidgetData(RenderWidgetInput renderWidgetInput)
|
||||
{
|
||||
this.type = renderWidgetInput.getWidgetMetaData().getType();
|
||||
this.queryParams = renderWidgetInput.getQueryParams();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String getType()
|
||||
{
|
||||
return (type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryParams
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getQueryParams()
|
||||
{
|
||||
return queryParams;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.dashboard.widgets;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.DividerWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Generic widget for showing a divider
|
||||
*******************************************************************************/
|
||||
public class DividerWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(input);
|
||||
return (new RenderWidgetOutput(new DividerWidgetData()));
|
||||
}
|
||||
}
|
@ -22,24 +22,12 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ParentWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -65,43 +53,13 @@ public class ParentWidgetRenderer extends AbstractWidgetRenderer
|
||||
/////////////////////////////////////////////////////////////
|
||||
// handle any PVSs creating dropdown data for the frontend //
|
||||
/////////////////////////////////////////////////////////////
|
||||
List<List<Map<String, String>>> pvsData = new ArrayList<>();
|
||||
List<String> pvsLabels = new ArrayList<>();
|
||||
List<String> pvsNames = new ArrayList<>();
|
||||
for(String possibleValueSourceName : CollectionUtils.nonNullList(metaData.getPossibleValueNameList()))
|
||||
boolean dropdownsValid = setupDropdowns(input, metaData, widgetData);
|
||||
|
||||
if(dropdownsValid)
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
|
||||
pvsLabels.add(possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
|
||||
pvsNames.add(possibleValueSourceName);
|
||||
|
||||
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput(input.getInstance());
|
||||
pvsInput.setSession(input.getSession());
|
||||
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
|
||||
|
||||
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
|
||||
pvsData.add(dropdownOptionList);
|
||||
|
||||
//////////////////////////////////////////
|
||||
// sort results, dedupe, and add to map //
|
||||
//////////////////////////////////////////
|
||||
Set<String> exists = new HashSet<>();
|
||||
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
|
||||
output.getResults().sort(Comparator.comparing(QPossibleValue::getLabel));
|
||||
for(QPossibleValue<?> possibleValue : output.getResults())
|
||||
{
|
||||
dropdownOptionList.add(Map.of(
|
||||
"id", String.valueOf(possibleValue.getId()),
|
||||
"label", possibleValue.getLabel()
|
||||
));
|
||||
}
|
||||
widgetData.setChildWidgetNameList(metaData.getChildWidgetNameList());
|
||||
}
|
||||
|
||||
widgetData.setDropdownNameList(pvsNames);
|
||||
widgetData.setDropdownLabelList(pvsLabels);
|
||||
widgetData.setDropdownDataList(pvsData);
|
||||
widgetData.setChildWidgetNameList(metaData.getChildWidgetNameList());
|
||||
|
||||
return (new RenderWidgetOutput(widgetData));
|
||||
}
|
||||
catch(Exception e)
|
||||
|
@ -22,6 +22,7 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
@ -53,9 +54,13 @@ public class ProcessWidgetRenderer extends AbstractWidgetRenderer
|
||||
ProcessWidgetData data = new ProcessWidgetData();
|
||||
if(input.getWidgetMetaData() instanceof QWidgetMetaData widgetMetaData)
|
||||
{
|
||||
setupDropdowns(input, widgetMetaData, data);
|
||||
|
||||
String processName = (String) widgetMetaData.getDefaultValues().get(WIDGET_PROCESS_NAME);
|
||||
QProcessMetaData processMetaData = input.getInstance().getProcess(processName);
|
||||
data.setProcessMetaData(processMetaData);
|
||||
|
||||
data.setDefaultValues(new HashMap<>(input.getQueryParams()));
|
||||
}
|
||||
return (new RenderWidgetOutput(data));
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidget;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QuickSightChart;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QuickSightChartMetaData;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
@ -74,8 +74,8 @@ public class QuickSightChartRenderer extends AbstractWidgetRenderer
|
||||
|
||||
final GenerateEmbedUrlForRegisteredUserResponse generateEmbedUrlForRegisteredUserResponse = quickSightClient.generateEmbedUrlForRegisteredUser(generateEmbedUrlForRegisteredUserRequest);
|
||||
|
||||
String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl();
|
||||
QWidget widget = new QuickSightChart(input.getWidgetMetaData().getName(), quickSightMetaData.getLabel(), embedUrl);
|
||||
String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl();
|
||||
QWidgetData widget = new QuickSightChart(input.getWidgetMetaData().getName(), quickSightMetaData.getLabel(), embedUrl);
|
||||
return (new RenderWidgetOutput(widget));
|
||||
}
|
||||
catch(Exception e)
|
||||
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.dashboard.widgets;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.USMapWidgetData;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Generic widget for display a map of the us
|
||||
*******************************************************************************/
|
||||
public class USMapWidgetRenderer extends AbstractWidgetRenderer
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
|
||||
{
|
||||
return (new RenderWidgetOutput(
|
||||
new USMapWidgetData()
|
||||
.withHeight("250px")
|
||||
.withMapMarkerList(generateMapMarkerList())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected List<USMapWidgetData.MapMarker> generateMapMarkerList() throws QException
|
||||
{
|
||||
return (List.of(new USMapWidgetData.MapMarker("maryville", new BigDecimal("38.725278"), new BigDecimal("-89.957778"))));
|
||||
}
|
||||
|
||||
}
|
@ -22,9 +22,12 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.interfaces;
|
||||
|
||||
|
||||
import java.util.HashSet;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -37,4 +40,34 @@ public interface GetInterface
|
||||
**
|
||||
*******************************************************************************/
|
||||
GetOutput execute(GetInput getInput) throws QException;
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
default void validateInput(GetInput getInput) throws QException
|
||||
{
|
||||
if(getInput.getPrimaryKey() != null & getInput.getUniqueKey() != null)
|
||||
{
|
||||
throw new QException("A GetInput may not contain both a primary key [" + getInput.getPrimaryKey() + "] and unique key [" + getInput.getUniqueKey() + "]");
|
||||
}
|
||||
|
||||
if(getInput.getUniqueKey() != null)
|
||||
{
|
||||
QTableMetaData table = getInput.getTable();
|
||||
boolean foundMatch = false;
|
||||
for(UniqueKey uniqueKey : table.getUniqueKeys())
|
||||
{
|
||||
if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet()))
|
||||
{
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!foundMatch)
|
||||
{
|
||||
throw new QException("Table [" + table.getName() + "] does not have a unique key defined on fields: " + getInput.getUniqueKey().keySet().stream().sorted().toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
|
||||
@ -40,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMeta
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
@ -70,10 +73,18 @@ public class MetaDataAction
|
||||
Map<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QTableMetaData> entry : metaDataInput.getInstance().getTables().entrySet())
|
||||
{
|
||||
String tableName = entry.getKey();
|
||||
String tableName = entry.getKey();
|
||||
QTableMetaData table = entry.getValue();
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
|
||||
tables.put(tableName, new QFrontendTableMetaData(backendForTable, entry.getValue(), false));
|
||||
treeNodes.put(tableName, new AppTreeNode(entry.getValue()));
|
||||
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false));
|
||||
treeNodes.put(tableName, new AppTreeNode(table));
|
||||
}
|
||||
metaDataOutput.setTables(tables);
|
||||
|
||||
@ -83,8 +94,17 @@ public class MetaDataAction
|
||||
Map<String, QFrontendProcessMetaData> processes = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QProcessMetaData> entry : metaDataInput.getInstance().getProcesses().entrySet())
|
||||
{
|
||||
processes.put(entry.getKey(), new QFrontendProcessMetaData(entry.getValue(), false));
|
||||
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
|
||||
String processName = entry.getKey();
|
||||
QProcessMetaData process = entry.getValue();
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
processes.put(processName, new QFrontendProcessMetaData(metaDataInput, process, false));
|
||||
treeNodes.put(processName, new AppTreeNode(process));
|
||||
}
|
||||
metaDataOutput.setProcesses(processes);
|
||||
|
||||
@ -94,8 +114,17 @@ public class MetaDataAction
|
||||
Map<String, QFrontendReportMetaData> reports = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QReportMetaData> entry : metaDataInput.getInstance().getReports().entrySet())
|
||||
{
|
||||
reports.put(entry.getKey(), new QFrontendReportMetaData(entry.getValue(), false));
|
||||
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
|
||||
String reportName = entry.getKey();
|
||||
QReportMetaData report = entry.getValue();
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
reports.put(reportName, new QFrontendReportMetaData(metaDataInput, report, false));
|
||||
treeNodes.put(reportName, new AppTreeNode(report));
|
||||
}
|
||||
metaDataOutput.setReports(reports);
|
||||
|
||||
@ -105,7 +134,16 @@ public class MetaDataAction
|
||||
Map<String, QFrontendWidgetMetaData> widgets = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QWidgetMetaDataInterface> entry : metaDataInput.getInstance().getWidgets().entrySet())
|
||||
{
|
||||
widgets.put(entry.getKey(), new QFrontendWidgetMetaData(entry.getValue()));
|
||||
String widgetName = entry.getKey();
|
||||
QWidgetMetaDataInterface widget = entry.getValue();
|
||||
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
widgets.put(widgetName, new QFrontendWidgetMetaData(metaDataInput, widget));
|
||||
}
|
||||
metaDataOutput.setWidgets(widgets);
|
||||
|
||||
@ -115,14 +153,32 @@ public class MetaDataAction
|
||||
Map<String, QFrontendAppMetaData> apps = new LinkedHashMap<>();
|
||||
for(Map.Entry<String, QAppMetaData> entry : metaDataInput.getInstance().getApps().entrySet())
|
||||
{
|
||||
apps.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue()));
|
||||
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
|
||||
String appName = entry.getKey();
|
||||
QAppMetaData app = entry.getValue();
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(entry.getValue().getChildren()))
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, app);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
for(QAppChildMetaData child : entry.getValue().getChildren())
|
||||
continue;
|
||||
}
|
||||
|
||||
apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput));
|
||||
treeNodes.put(appName, new AppTreeNode(app));
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
|
||||
{
|
||||
for(QAppChildMetaData child : app.getChildren())
|
||||
{
|
||||
apps.get(entry.getKey()).addChild(new AppTreeNode(child));
|
||||
if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules)
|
||||
{
|
||||
PermissionCheckResult childPermissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules);
|
||||
if(childPermissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
apps.get(appName).addChild(new AppTreeNode(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +192,7 @@ public class MetaDataAction
|
||||
{
|
||||
if(appMetaData.getParentAppName() == null)
|
||||
{
|
||||
buildAppTree(treeNodes, appTree, appMetaData);
|
||||
buildAppTree(metaDataInput, treeNodes, appTree, appMetaData);
|
||||
}
|
||||
}
|
||||
metaDataOutput.setAppTree(appTree);
|
||||
@ -149,6 +205,8 @@ public class MetaDataAction
|
||||
metaDataOutput.setBranding(metaDataInput.getInstance().getBranding());
|
||||
}
|
||||
|
||||
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want?
|
||||
|
||||
return metaDataOutput;
|
||||
@ -159,7 +217,7 @@ public class MetaDataAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void buildAppTree(Map<String, AppTreeNode> treeNodes, List<AppTreeNode> nodeList, QAppChildMetaData childMetaData)
|
||||
private void buildAppTree(MetaDataInput metaDataInput, Map<String, AppTreeNode> treeNodes, List<AppTreeNode> nodeList, QAppChildMetaData childMetaData)
|
||||
{
|
||||
AppTreeNode treeNode = treeNodes.get(childMetaData.getName());
|
||||
if(treeNode == null)
|
||||
@ -174,7 +232,16 @@ public class MetaDataAction
|
||||
{
|
||||
for(QAppChildMetaData child : app.getChildren())
|
||||
{
|
||||
buildAppTree(treeNodes, treeNode.getChildren(), child);
|
||||
if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules)
|
||||
{
|
||||
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules);
|
||||
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
buildAppTree(metaDataInput, treeNodes, treeNode.getChildren(), child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ public class ProcessMetaDataAction
|
||||
{
|
||||
throw (new QNotFoundException("Process [" + processMetaDataInput.getProcessName() + "] was not found."));
|
||||
}
|
||||
processMetaDataOutput.setProcess(new QFrontendProcessMetaData(process, true));
|
||||
processMetaDataOutput.setProcess(new QFrontendProcessMetaData(processMetaDataInput, process, true));
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
|
@ -54,7 +54,7 @@ public class TableMetaDataAction
|
||||
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
|
||||
}
|
||||
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
|
||||
tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true));
|
||||
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true));
|
||||
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
|
@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QField;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class AvailablePermission extends QRecordEntity
|
||||
{
|
||||
public static final String TABLE_NAME = "availablePermission";
|
||||
|
||||
@QField(label = "Permission Name")
|
||||
private String name;
|
||||
|
||||
@QField(label = "Object")
|
||||
private String objectName;
|
||||
|
||||
@QField()
|
||||
private String objectType;
|
||||
|
||||
@QField()
|
||||
private String permissionType;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
AvailablePermission that = (AvailablePermission) o;
|
||||
return Objects.equals(name, that.name) && Objects.equals(objectName, that.objectName) && Objects.equals(objectType, that.objectType) && Objects.equals(permissionType, that.permissionType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hash(name, objectName, objectType, permissionType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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 AvailablePermission withName(String name)
|
||||
{
|
||||
this.name = name;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for objectType
|
||||
*******************************************************************************/
|
||||
public String getObjectType()
|
||||
{
|
||||
return (this.objectType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for objectType
|
||||
*******************************************************************************/
|
||||
public void setObjectType(String objectType)
|
||||
{
|
||||
this.objectType = objectType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for objectType
|
||||
*******************************************************************************/
|
||||
public AvailablePermission withObjectType(String objectType)
|
||||
{
|
||||
this.objectType = objectType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for permissionType
|
||||
*******************************************************************************/
|
||||
public String getPermissionType()
|
||||
{
|
||||
return (this.permissionType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for permissionType
|
||||
*******************************************************************************/
|
||||
public void setPermissionType(String permissionType)
|
||||
{
|
||||
this.permissionType = permissionType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for permissionType
|
||||
*******************************************************************************/
|
||||
public AvailablePermission withPermissionType(String permissionType)
|
||||
{
|
||||
this.permissionType = permissionType;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for objectName
|
||||
*******************************************************************************/
|
||||
public String getObjectName()
|
||||
{
|
||||
return (this.objectName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for objectName
|
||||
*******************************************************************************/
|
||||
public void setObjectName(String objectName)
|
||||
{
|
||||
this.objectName = objectName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for objectName
|
||||
*******************************************************************************/
|
||||
public AvailablePermission withObjectName(String objectName)
|
||||
{
|
||||
this.objectName = objectName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class BulkTableActionProcessPermissionChecker implements CustomPermissionChecker
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(BulkTableActionProcessPermissionChecker.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
|
||||
{
|
||||
String processName = metaDataWithPermissionRules.getName();
|
||||
if(processName != null && processName.indexOf('.') > -1)
|
||||
{
|
||||
String[] parts = processName.split("\\.", 2);
|
||||
String tableName = parts[0];
|
||||
String bulkActionName = parts[1];
|
||||
|
||||
AbstractTableActionInput tableActionInput = new AbstractTableActionInput();
|
||||
tableActionInput.setTableName(tableName);
|
||||
|
||||
switch(bulkActionName)
|
||||
{
|
||||
case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT);
|
||||
case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
|
||||
case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE);
|
||||
default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface CustomPermissionChecker
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException;
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum PermissionCheckResult
|
||||
{
|
||||
ALLOW,
|
||||
DENY_HIDE,
|
||||
DENY_DISABLE;
|
||||
}
|
@ -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.permissions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
sealed interface PermissionSubType permits PrivatePermissionSubType, TablePermissionSubType
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
String getPermissionSuffix();
|
||||
|
||||
}
|
@ -0,0 +1,588 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithName;
|
||||
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;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
|
||||
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.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class PermissionsHelper
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(PermissionsHelper.class);
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void checkTablePermissionThrowing(AbstractTableActionInput tableActionInput, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
|
||||
{
|
||||
checkTablePermissionThrowing(tableActionInput, tableActionInput.getTableName(), permissionSubType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
|
||||
{
|
||||
warnAboutPermissionSubTypeForTables(permissionSubType);
|
||||
QTableMetaData table = actionInput.getInstance().getTable(tableName);
|
||||
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(table, actionInput.getInstance()), permissionSubType, table.getName(), actionInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static boolean hasTablePermission(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType)
|
||||
{
|
||||
try
|
||||
{
|
||||
checkTablePermissionThrowing(actionInput, tableName, permissionSubType);
|
||||
return (true);
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules)
|
||||
{
|
||||
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, actionInput.getInstance());
|
||||
String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName());
|
||||
|
||||
switch(rules.getLevel())
|
||||
{
|
||||
case NOT_PROTECTED:
|
||||
{
|
||||
/////////////////////////////////////////////////
|
||||
// if the entity isn't protected, always ALLOW //
|
||||
/////////////////////////////////////////////////
|
||||
return PermissionCheckResult.ALLOW;
|
||||
}
|
||||
case HAS_ACCESS_PERMISSION:
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// if the entity just has a 'has access', then check for 'has access' //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
|
||||
}
|
||||
case READ_WRITE_PERMISSIONS:
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// if the table is configured w/ read/write, check for either //
|
||||
////////////////////////////////////////////////////////////////
|
||||
if(metaDataWithPermissionRules instanceof QTableMetaData)
|
||||
{
|
||||
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.READ, PrivatePermissionSubType.WRITE);
|
||||
}
|
||||
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
|
||||
}
|
||||
case READ_INSERT_EDIT_DELETE_PERMISSIONS:
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// if the table is configured w/ read/insert/edit/delete, check for any //
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
if(metaDataWithPermissionRules instanceof QTableMetaData)
|
||||
{
|
||||
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, TablePermissionSubType.READ, TablePermissionSubType.INSERT, TablePermissionSubType.EDIT, TablePermissionSubType.DELETE);
|
||||
}
|
||||
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
|
||||
}
|
||||
default:
|
||||
{
|
||||
return getPermissionDeniedCheckResult(rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName) throws QPermissionDeniedException
|
||||
{
|
||||
checkProcessPermissionThrowing(actionInput, processName, Collections.emptyMap());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
|
||||
{
|
||||
QProcessMetaData process = actionInput.getInstance().getProcess(processName);
|
||||
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, actionInput.getInstance());
|
||||
|
||||
if(effectivePermissionRules.getCustomPermissionChecker() != null)
|
||||
{
|
||||
/////////////////////////////////////
|
||||
// todo - avoid stack overflows... //
|
||||
/////////////////////////////////////
|
||||
CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, effectivePermissionRules.getCustomPermissionChecker());
|
||||
customPermissionChecker.checkPermissionsThrowing(actionInput, process);
|
||||
return;
|
||||
}
|
||||
|
||||
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName(), actionInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static boolean hasProcessPermission(AbstractActionInput actionInput, String processName)
|
||||
{
|
||||
try
|
||||
{
|
||||
checkProcessPermissionThrowing(actionInput, processName);
|
||||
return (true);
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
|
||||
{
|
||||
QAppMetaData app = actionInput.getInstance().getApp(appName);
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(app, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static boolean hasAppPermission(AbstractActionInput actionInput, String appName)
|
||||
{
|
||||
try
|
||||
{
|
||||
checkAppPermissionThrowing(actionInput, appName);
|
||||
return (true);
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
|
||||
{
|
||||
QReportMetaData report = actionInput.getInstance().getReport(reportName);
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(report, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static boolean hasReportPermission(AbstractActionInput actionInput, String reportName)
|
||||
{
|
||||
try
|
||||
{
|
||||
checkReportPermissionThrowing(actionInput, reportName);
|
||||
return (true);
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
|
||||
{
|
||||
QWidgetMetaDataInterface widget = actionInput.getInstance().getWidget(widgetName);
|
||||
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static boolean hasWidgetPermission(AbstractActionInput actionInput, String widgetName)
|
||||
{
|
||||
try
|
||||
{
|
||||
checkWidgetPermissionThrowing(actionInput, widgetName);
|
||||
return (true);
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Collection<String> getAllAvailablePermissionNames(QInstance instance)
|
||||
{
|
||||
return (getAllAvailablePermissions(instance).stream()
|
||||
.map(AvailablePermission::getName)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Collection<AvailablePermission> getAllAvailablePermissions(QInstance instance)
|
||||
{
|
||||
Collection<AvailablePermission> rs = new LinkedHashSet<>();
|
||||
|
||||
for(QTableMetaData tableMetaData : instance.getTables().values())
|
||||
{
|
||||
if(tableMetaData.getIsHidden())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QPermissionRules rules = getEffectivePermissionRules(tableMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, tableMetaData.getName());
|
||||
|
||||
for(TablePermissionSubType permissionSubType : TablePermissionSubType.values())
|
||||
{
|
||||
addEffectiveAvailablePermission(rules, permissionSubType, rs, baseName, tableMetaData, "Table");
|
||||
}
|
||||
}
|
||||
|
||||
for(QProcessMetaData processMetaData : instance.getProcesses().values())
|
||||
{
|
||||
if(processMetaData.getIsHidden())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QPermissionRules rules = getEffectivePermissionRules(processMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, processMetaData.getName());
|
||||
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, processMetaData, "Process");
|
||||
}
|
||||
|
||||
for(QAppMetaData appMetaData : instance.getApps().values())
|
||||
{
|
||||
QPermissionRules rules = getEffectivePermissionRules(appMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, appMetaData.getName());
|
||||
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, appMetaData, "App");
|
||||
}
|
||||
|
||||
for(QReportMetaData reportMetaData : instance.getReports().values())
|
||||
{
|
||||
QPermissionRules rules = getEffectivePermissionRules(reportMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, reportMetaData.getName());
|
||||
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, reportMetaData, "Report");
|
||||
}
|
||||
|
||||
for(QWidgetMetaDataInterface widgetMetaData : instance.getWidgets().values())
|
||||
{
|
||||
QPermissionRules rules = getEffectivePermissionRules(widgetMetaData, instance);
|
||||
String baseName = getEffectivePermissionBaseName(rules, widgetMetaData.getName());
|
||||
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void addEffectiveAvailablePermission(QPermissionRules rules, PermissionSubType permissionSubType, Collection<AvailablePermission> rs, String baseName, MetaDataWithName metaDataWithName, String objectType)
|
||||
{
|
||||
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
|
||||
if(effectivePermissionSubType != null)
|
||||
{
|
||||
rs.add(new AvailablePermission()
|
||||
.withName(getPermissionName(baseName, effectivePermissionSubType))
|
||||
.withObjectName(metaDataWithName.getLabel())
|
||||
.withPermissionType(effectivePermissionSubType.toString())
|
||||
.withObjectType(objectType));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QPermissionRules getEffectivePermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules, QInstance instance)
|
||||
{
|
||||
if(metaDataWithPermissionRules.getPermissionRules() == null)
|
||||
{
|
||||
LOG.warn("Null permission rules on meta data object [" + metaDataWithPermissionRules.getClass().getSimpleName() + "][" + metaDataWithPermissionRules.getName() + "] - does the instance need enriched? Returning instance default rules.");
|
||||
return (instance.getDefaultPermissionRules());
|
||||
}
|
||||
return (metaDataWithPermissionRules.getPermissionRules());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static boolean hasPermission(QSession session, String permissionBaseName, PermissionSubType permissionSubType)
|
||||
{
|
||||
if(permissionSubType == null)
|
||||
{
|
||||
return (true);
|
||||
}
|
||||
|
||||
String permissionName = getPermissionName(permissionBaseName, permissionSubType);
|
||||
return (session.hasPermission(permissionName));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, QPermissionRules rules, String permissionBaseName, MetaDataWithPermissionRules metaDataWithPermissionRules, PermissionSubType... permissionSubTypes)
|
||||
{
|
||||
for(PermissionSubType permissionSubType : permissionSubTypes)
|
||||
{
|
||||
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
|
||||
|
||||
if(rules.getCustomPermissionChecker() != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, rules.getCustomPermissionChecker());
|
||||
customPermissionChecker.checkPermissionsThrowing(actionInput, metaDataWithPermissionRules);
|
||||
return (PermissionCheckResult.ALLOW);
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
return (getPermissionDeniedCheckResult(rules));
|
||||
}
|
||||
}
|
||||
|
||||
if(hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
|
||||
{
|
||||
return (PermissionCheckResult.ALLOW);
|
||||
}
|
||||
}
|
||||
|
||||
return (getPermissionDeniedCheckResult(rules));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static String getEffectivePermissionBaseName(QPermissionRules rules, String standardName)
|
||||
{
|
||||
if(rules != null && StringUtils.hasContent(rules.getPermissionBaseName()))
|
||||
{
|
||||
return (rules.getPermissionBaseName());
|
||||
}
|
||||
|
||||
return (standardName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@SuppressWarnings("checkstyle:indentation")
|
||||
static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType)
|
||||
{
|
||||
if(rules == null || rules.getLevel() == null)
|
||||
{
|
||||
return (originalPermissionSubType);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the original permission sub-type is "hasAccess" - then this is a check for a process/report/widget. //
|
||||
// in that case - never return the table-level read/write/insert/edit/delete options //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType))
|
||||
{
|
||||
return switch(rules.getLevel())
|
||||
{
|
||||
case NOT_PROTECTED -> null;
|
||||
default -> PrivatePermissionSubType.HAS_ACCESS;
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else, this is a table check - so - based on the rules being used for this table, map the requested //
|
||||
// permission sub-type to what we expect to be set for the table //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return switch(rules.getLevel())
|
||||
{
|
||||
case NOT_PROTECTED -> null;
|
||||
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
|
||||
case READ_WRITE_PERMISSIONS ->
|
||||
{
|
||||
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (originalPermissionSubType);
|
||||
}
|
||||
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (PrivatePermissionSubType.WRITE);
|
||||
}
|
||||
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
|
||||
{
|
||||
yield (PrivatePermissionSubType.READ);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
|
||||
}
|
||||
}
|
||||
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name, AbstractActionInput actionInput) throws QPermissionDeniedException
|
||||
{
|
||||
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
|
||||
String permissionBaseName = getEffectivePermissionBaseName(rules, name);
|
||||
|
||||
if(effectivePermissionSubType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
|
||||
{
|
||||
// LOG.debug("Throwing permission denied for: " + getPermissionName(permissionBaseName, effectivePermissionSubType) + " for " + actionInput.getSession().getUser());
|
||||
throw (new QPermissionDeniedException("Permission denied."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static String getPermissionName(String permissionBaseName, PermissionSubType permissionSubType)
|
||||
{
|
||||
return permissionBaseName + "." + permissionSubType.getPermissionSuffix();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static PermissionCheckResult getPermissionDeniedCheckResult(QPermissionRules rules)
|
||||
{
|
||||
if(rules == null || rules.getDenyBehavior() == null || rules.getDenyBehavior().equals(DenyBehavior.HIDDEN))
|
||||
{
|
||||
return (PermissionCheckResult.DENY_HIDE);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (PermissionCheckResult.DENY_DISABLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void warnAboutPermissionSubTypeForTables(PermissionSubType permissionSubType)
|
||||
{
|
||||
if(permissionSubType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(permissionSubType == PrivatePermissionSubType.HAS_ACCESS)
|
||||
{
|
||||
LOG.warn("PermissionSubType.HAS_ACCESS should not be checked for a table");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
enum PrivatePermissionSubType implements PermissionSubType
|
||||
{
|
||||
HAS_ACCESS("hasAccess"), // for processes, reports, etc - basically, not tables.
|
||||
READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode
|
||||
WRITE("write");
|
||||
|
||||
private final String permissionSuffix;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
PrivatePermissionSubType(String permissionSuffix)
|
||||
{
|
||||
this.permissionSuffix = permissionSuffix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for permissionSuffix
|
||||
*******************************************************************************/
|
||||
public String getPermissionSuffix()
|
||||
{
|
||||
return (this.permissionSuffix);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class ReportProcessPermissionChecker implements CustomPermissionChecker
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
|
||||
{
|
||||
if(actionInput instanceof RunProcessInput runProcessInput)
|
||||
{
|
||||
String reportName = runProcessInput.getValueString("reportName");
|
||||
if(reportName != null)
|
||||
{
|
||||
PermissionsHelper.checkReportPermissionThrowing(actionInput, reportName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.permissions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public enum TablePermissionSubType implements PermissionSubType
|
||||
{
|
||||
READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode
|
||||
INSERT("insert"), // for table-insert.
|
||||
EDIT("edit"), // for table-edit.
|
||||
DELETE("delete"); // for table-delete.
|
||||
|
||||
private final String permissionSuffix;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
TablePermissionSubType(String permissionSuffix)
|
||||
{
|
||||
this.permissionSuffix = permissionSuffix;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for permissionSuffix
|
||||
*******************************************************************************/
|
||||
public String getPermissionSuffix()
|
||||
{
|
||||
return (this.permissionSuffix);
|
||||
}
|
||||
|
||||
}
|
@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.tables.query.QueryInput;
|
||||
@ -42,8 +43,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
|
||||
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.utils.CollectionUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -52,7 +51,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class RunBackendStepAction
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(RunBackendStepAction.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(RunBackendStepAction.class);
|
||||
|
||||
|
||||
|
||||
@ -107,7 +106,7 @@ public class RunBackendStepAction
|
||||
return;
|
||||
}
|
||||
|
||||
List<QFieldMetaData> fieldsToGet = new ArrayList<>();
|
||||
List<QFieldMetaData> fieldsToGet = new ArrayList<>();
|
||||
List<QFieldMetaData> requiredFieldsMissing = new ArrayList<>();
|
||||
for(QFieldMetaData field : inputMetaData.getFieldList())
|
||||
{
|
||||
@ -175,8 +174,7 @@ public class RunBackendStepAction
|
||||
{
|
||||
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(runBackendStepInput.getInstance());
|
||||
queryInput.setSession(runBackendStepInput.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
|
||||
|
||||
// todo - handle this being async (e.g., http)
|
||||
|
@ -35,6 +35,7 @@ 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.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||
@ -48,6 +49,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.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
@ -60,8 +62,6 @@ import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.BooleanUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -70,7 +70,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class RunProcessAction
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(RunProcessAction.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
|
||||
|
||||
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
|
||||
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
|
||||
@ -80,6 +80,7 @@ public class RunProcessAction
|
||||
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp";
|
||||
public static final String BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD = "basepullDidQueryUsingTimestamp";
|
||||
|
||||
|
||||
|
||||
@ -142,7 +143,7 @@ public class RunProcessAction
|
||||
QStepMetaData step = stepList.get(0);
|
||||
lastStepName = step.getName();
|
||||
|
||||
if(step instanceof QFrontendStepMetaData)
|
||||
if(step instanceof QFrontendStepMetaData frontendStep)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////
|
||||
// Handle what to do with frontend steps, per request setting //
|
||||
@ -152,6 +153,7 @@ public class RunProcessAction
|
||||
case BREAK ->
|
||||
{
|
||||
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
|
||||
processFrontendStepFieldDefaultValues(processState, frontendStep);
|
||||
processState.setNextStepName(step.getName());
|
||||
break STEP_LOOP;
|
||||
}
|
||||
@ -190,11 +192,14 @@ public class RunProcessAction
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
// if 'basepull' style process, update the stored basepull timestamp //
|
||||
// but only when we've been signaled to do so - i.e., after an Execute step runs. //
|
||||
////////////////////////////////////////////////////////////////////////////////////
|
||||
if(basepullConfiguration != null && BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD))))
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if 'basepull' style process, update the stored basepull timestamp //
|
||||
// but only when we've been signaled to do so - i.e., only if we did our //
|
||||
// query using the timestamp field, and only after an Execute step runs. //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
if(basepullConfiguration != null
|
||||
&& BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD)))
|
||||
&& BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD))))
|
||||
{
|
||||
storeLastRunTime(runProcessInput, process, basepullConfiguration);
|
||||
}
|
||||
@ -226,6 +231,22 @@ public class RunProcessAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void processFrontendStepFieldDefaultValues(ProcessState processState, QFrontendStepMetaData step)
|
||||
{
|
||||
for(QFieldMetaData formField : CollectionUtils.mergeLists(step.getFormFields(), step.getInputFields(), step.getViewFields(), step.getOutputFields()))
|
||||
{
|
||||
if(formField.getDefaultValue() != null && processState.getValues().get(formField.getName()) == null)
|
||||
{
|
||||
processState.getValues().put(formField.getName(), formField.getDefaultValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** When we start running a process (or resuming it), get data in the RunProcessRequest
|
||||
** either from the state provider (if they're found, for a resume).
|
||||
@ -291,11 +312,10 @@ public class RunProcessAction
|
||||
*******************************************************************************/
|
||||
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
|
||||
{
|
||||
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getInstance(), processState);
|
||||
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
|
||||
runBackendStepInput.setProcessName(process.getName());
|
||||
runBackendStepInput.setStepName(backendStep.getName());
|
||||
runBackendStepInput.setTableName(process.getTableName());
|
||||
runBackendStepInput.setSession(runProcessInput.getSession());
|
||||
runBackendStepInput.setCallback(runProcessInput.getCallback());
|
||||
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
|
||||
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
|
||||
@ -440,8 +460,7 @@ public class RunProcessAction
|
||||
///////////////////////////////////////
|
||||
// get the stored basepull timestamp //
|
||||
///////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
|
||||
queryInput.setSession(runProcessInput.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(basepullTableName);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(
|
||||
new QFilterCriteria()
|
||||
@ -469,8 +488,7 @@ public class RunProcessAction
|
||||
////////////
|
||||
// update //
|
||||
////////////
|
||||
UpdateInput updateInput = new UpdateInput(runProcessInput.getInstance());
|
||||
updateInput.setSession(runProcessInput.getSession());
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(basepullTableName);
|
||||
updateInput.setRecords(List.of(basepullRecord));
|
||||
new UpdateAction().execute(updateInput);
|
||||
@ -484,8 +502,7 @@ public class RunProcessAction
|
||||
////////////////////////////////
|
||||
// insert new basepull record //
|
||||
////////////////////////////////
|
||||
InsertInput insertInput = new InsertInput(runProcessInput.getInstance());
|
||||
insertInput.setSession(runProcessInput.getSession());
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(basepullTableName);
|
||||
insertInput.setRecords(List.of(basepullRecord));
|
||||
new InsertAction().execute(insertInput);
|
||||
@ -523,8 +540,7 @@ public class RunProcessAction
|
||||
///////////////////////////////////////
|
||||
// get the stored basepull timestamp //
|
||||
///////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
|
||||
queryInput.setSession(runProcessInput.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(basepullTableName);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(
|
||||
new QFilterCriteria()
|
||||
|
@ -22,25 +22,27 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.queues;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.sqs.AmazonSQS;
|
||||
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
|
||||
import com.amazonaws.services.sqs.model.DeleteMessageRequest;
|
||||
import com.amazonaws.services.sqs.model.DeleteMessageBatchRequest;
|
||||
import com.amazonaws.services.sqs.model.DeleteMessageBatchRequestEntry;
|
||||
import com.amazonaws.services.sqs.model.Message;
|
||||
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
|
||||
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.scheduler.StandardScheduledExecutor;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -48,7 +50,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class SQSQueuePoller implements Runnable
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(SQSQueuePoller.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(SQSQueuePoller.class);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// todo - move these 2 to a "QBaseRunnable"? //
|
||||
@ -67,8 +69,10 @@ public class SQSQueuePoller implements Runnable
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
QContext.init(qInstance, sessionSupplier.get());
|
||||
|
||||
String originalThreadName = Thread.currentThread().getName();
|
||||
Thread.currentThread().setName("SQSPoller>" + queueMetaData.getName() + StandardScheduledExecutor.newThreadNameRandomSuffix());
|
||||
Thread.currentThread().setName("SQSPoller>" + queueMetaData.getName());
|
||||
LOG.debug("Running " + this.getClass().getSimpleName() + "[" + queueMetaData.getName() + "]");
|
||||
|
||||
try
|
||||
@ -88,8 +92,13 @@ public class SQSQueuePoller implements Runnable
|
||||
|
||||
while(true)
|
||||
{
|
||||
///////////////////////////////
|
||||
// fetch a batch of messages //
|
||||
///////////////////////////////
|
||||
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest();
|
||||
receiveMessageRequest.setQueueUrl(queueUrl);
|
||||
receiveMessageRequest.setMaxNumberOfMessages(10);
|
||||
receiveMessageRequest.setWaitTimeSeconds(20); // help urge SQS to query multiple servers and find more messages
|
||||
ReceiveMessageResult receiveMessageResult = sqs.receiveMessage(receiveMessageRequest);
|
||||
if(receiveMessageResult.getMessages().isEmpty())
|
||||
{
|
||||
@ -98,35 +107,65 @@ public class SQSQueuePoller implements Runnable
|
||||
}
|
||||
LOG.debug(receiveMessageResult.getMessages().size() + " messages received. Processing.");
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// extract data from the messages into list of bodies to pass into process, and list of delete-batch-inputs //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
List<DeleteMessageBatchRequestEntry> deleteRequestEntries = new ArrayList<>();
|
||||
ArrayList<String> bodies = new ArrayList<>();
|
||||
int i = 0;
|
||||
for(Message message : receiveMessageResult.getMessages())
|
||||
{
|
||||
String body = message.getBody();
|
||||
bodies.add(message.getBody());
|
||||
deleteRequestEntries.add(new DeleteMessageBatchRequestEntry(String.valueOf(i++), message.getReceiptHandle()));
|
||||
}
|
||||
|
||||
RunProcessInput runProcessInput = new RunProcessInput(qInstance);
|
||||
runProcessInput.setSession(sessionSupplier.get());
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// run the process, in a try-catch, so even if it fails, our loop keeps going. //
|
||||
// the messages in a failed process will get re-delivered, to try-again, up to the //
|
||||
// number of times configured in AWS //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
RunProcessInput runProcessInput = new RunProcessInput();
|
||||
runProcessInput.setProcessName(queueMetaData.getProcessName());
|
||||
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
|
||||
runProcessInput.addValue("body", body);
|
||||
runProcessInput.addValue("bodies", bodies);
|
||||
|
||||
RunProcessAction runProcessAction = new RunProcessAction();
|
||||
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
|
||||
|
||||
/////////////////////////////////
|
||||
// todo - what of exceptions?? //
|
||||
/////////////////////////////////
|
||||
|
||||
String receiptHandle = message.getReceiptHandle();
|
||||
sqs.deleteMessage(new DeleteMessageRequest(queueUrl, receiptHandle));
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there was an exception returned by the process (e.g., thrown in backend step), then //
|
||||
// warn and leave the messages for re-processing. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(runProcessOutput.getException().isPresent())
|
||||
{
|
||||
LOG.warn("Exception returned by process when handling SQS Messages. They will not be deleted from the queue.", runProcessOutput.getException().get());
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////
|
||||
// else, if no exception, do a batch delete. //
|
||||
///////////////////////////////////////////////
|
||||
sqs.deleteMessageBatch(new DeleteMessageBatchRequest()
|
||||
.withQueueUrl(queueUrl)
|
||||
.withEntries(deleteRequestEntries));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error receiving SQS Messages.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error receiving SQS Message", e);
|
||||
LOG.warn("Error running SQS Queue Poller", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Thread.currentThread().setName(originalThreadName);
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.reporting;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Subclass of RecordPipe, which uses a buffer in the addRecord method, to avoid
|
||||
** sending single-records at a time through postRecordActions and to consumers.
|
||||
*******************************************************************************/
|
||||
public class BufferedRecordPipe extends RecordPipe
|
||||
{
|
||||
private List<QRecord> buffer = new ArrayList<>();
|
||||
private Integer bufferSize = 100;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor - uses default buffer size
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BufferedRecordPipe()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor - customize buffer size.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public BufferedRecordPipe(Integer bufferSize)
|
||||
{
|
||||
this.bufferSize = bufferSize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addRecord(QRecord record)
|
||||
{
|
||||
buffer.add(record);
|
||||
if(buffer.size() >= bufferSize)
|
||||
{
|
||||
addRecords(buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void finalFlush()
|
||||
{
|
||||
if(!buffer.isEmpty())
|
||||
{
|
||||
addRecords(buffer);
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
@ -27,13 +27,12 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.adapters.QRecordToCsvAdapter;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.utils.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -41,7 +40,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class CsvExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(CsvExportStreamer.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(CsvExportStreamer.class);
|
||||
|
||||
private final QRecordToCsvAdapter qRecordToCsvAdapter;
|
||||
|
||||
|
@ -37,6 +37,7 @@ import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
@ -45,8 +46,6 @@ 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.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.dhatim.fastexcel.StyleSetter;
|
||||
import org.dhatim.fastexcel.Workbook;
|
||||
import org.dhatim.fastexcel.Worksheet;
|
||||
@ -57,7 +56,7 @@ import org.dhatim.fastexcel.Worksheet;
|
||||
*******************************************************************************/
|
||||
public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(ExcelExportStreamer.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
|
||||
|
||||
private ExportInput exportInput;
|
||||
private QTableMetaData table;
|
||||
@ -247,14 +246,6 @@ public class ExcelExportStreamer implements ExportStreamerInterface
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
Serializable value = qRecord.getValue(field.getName());
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
String displayValue = qRecord.getDisplayValue(field.getName());
|
||||
if(displayValue != null)
|
||||
{
|
||||
value = displayValue;
|
||||
}
|
||||
}
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
|
@ -31,10 +31,11 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
|
||||
@ -43,14 +44,13 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -66,7 +66,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class ExportAction
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(ExportAction.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(ExportAction.class);
|
||||
|
||||
private boolean preExecuteRan = false;
|
||||
private Integer countFromPreExecute = null;
|
||||
@ -147,17 +147,17 @@ public class ExportAction
|
||||
//////////////////////////
|
||||
// set up a query input //
|
||||
//////////////////////////
|
||||
QueryInterface queryInterface = backendModule.getQueryInterface();
|
||||
QueryInput queryInput = new QueryInput(exportInput.getInstance());
|
||||
queryInput.setSession(exportInput.getSession());
|
||||
QueryAction queryAction = new QueryAction();
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(exportInput.getTableName());
|
||||
queryInput.setFilter(exportInput.getQueryFilter());
|
||||
queryInput.setLimit(exportInput.getLimit());
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// tell this query that it needs to put its output into a pipe //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
RecordPipe recordPipe = new RecordPipe();
|
||||
RecordPipe recordPipe = new BufferedRecordPipe(500);
|
||||
queryInput.setRecordPipe(recordPipe);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -165,13 +165,14 @@ public class ExportAction
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ReportFormat reportFormat = exportInput.getReportFormat();
|
||||
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
|
||||
reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1");
|
||||
List<QFieldMetaData> fields = getFields(exportInput);
|
||||
reportStreamer.start(exportInput, fields, "Sheet 1");
|
||||
|
||||
//////////////////////////////////////////
|
||||
// run the query action as an async job //
|
||||
//////////////////////////////////////////
|
||||
AsyncJobManager asyncJobManager = new AsyncJobManager();
|
||||
String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryInterface.execute(queryInput)));
|
||||
String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryAction.execute(queryInput)));
|
||||
LOG.info("Started query job [" + queryJobUUID + "] for report");
|
||||
|
||||
AsyncJobState queryJobState = AsyncJobState.RUNNING;
|
||||
@ -209,7 +210,7 @@ public class ExportAction
|
||||
nextSleepMillis = INIT_SLEEP_MS;
|
||||
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
reportStreamer.addRecords(records);
|
||||
processRecords(reportStreamer, fields, records);
|
||||
recordCount += records.size();
|
||||
|
||||
LOG.info(countFromPreExecute != null
|
||||
@ -238,7 +239,7 @@ public class ExportAction
|
||||
// send the final records to the report streamer //
|
||||
///////////////////////////////////////////////////
|
||||
List<QRecord> records = recordPipe.consumeAvailableRecords();
|
||||
reportStreamer.addRecords(records);
|
||||
processRecords(reportStreamer, fields, records);
|
||||
recordCount += records.size();
|
||||
|
||||
long reportEndTime = System.currentTimeMillis();
|
||||
@ -269,20 +270,59 @@ public class ExportAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void processRecords(ExportStreamerInterface reportStreamer, List<QFieldMetaData> fields, List<QRecord> records) throws QReportingException
|
||||
{
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
if(field.getName().endsWith(":possibleValueLabel"))
|
||||
{
|
||||
String effectiveFieldName = field.getName().replace(":possibleValueLabel", "");
|
||||
for(QRecord record : records)
|
||||
{
|
||||
String displayValue = record.getDisplayValue(effectiveFieldName);
|
||||
record.setValue(field.getName(), displayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reportStreamer.addRecords(records);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private List<QFieldMetaData> getFields(ExportInput exportInput)
|
||||
{
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
List<QFieldMetaData> fieldList;
|
||||
QTableMetaData table = exportInput.getTable();
|
||||
if(exportInput.getFieldNames() != null)
|
||||
{
|
||||
return (exportInput.getFieldNames().stream().map(table::getField).toList());
|
||||
fieldList = exportInput.getFieldNames().stream().map(table::getField).toList();
|
||||
}
|
||||
else
|
||||
{
|
||||
return (new ArrayList<>(table.getFields().values()));
|
||||
fieldList = new ArrayList<>(table.getFields().values());
|
||||
}
|
||||
|
||||
//////////////////////////////////////////
|
||||
// add fields for possible value labels //
|
||||
//////////////////////////////////////////
|
||||
List<QFieldMetaData> returnList = new ArrayList<>();
|
||||
for(QFieldMetaData field : fieldList)
|
||||
{
|
||||
returnList.add(field);
|
||||
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
|
||||
{
|
||||
returnList.add(new QFieldMetaData(field.getName() + ":possibleValueLabel", QFieldType.STRING).withLabel(field.getLabel() + " Name"));
|
||||
}
|
||||
}
|
||||
|
||||
return (returnList);
|
||||
}
|
||||
|
||||
|
||||
@ -308,8 +348,7 @@ public class ExportAction
|
||||
if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows())
|
||||
{
|
||||
CountInterface countInterface = backendModule.getCountInterface();
|
||||
CountInput countInput = new CountInput(exportInput.getInstance());
|
||||
countInput.setSession(exportInput.getSession());
|
||||
CountInput countInput = new CountInput();
|
||||
countInput.setTableName(exportInput.getTableName());
|
||||
countInput.setFilter(exportInput.getQueryFilter());
|
||||
CountOutput countOutput = countInterface.execute(countInput);
|
||||
|
@ -27,13 +27,17 @@ import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
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.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
@ -41,6 +45,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.reporting.ExportInput;
|
||||
@ -84,6 +89,8 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
|
||||
*******************************************************************************/
|
||||
public class GenerateReportAction
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// summaryAggregates and varianceAggregates are multi-level maps, ala: //
|
||||
// viewName > SummaryKey > fieldName > Aggregates //
|
||||
@ -113,6 +120,10 @@ public class GenerateReportAction
|
||||
{
|
||||
report = reportInput.getInstance().getReport(reportInput.getReportName());
|
||||
reportFormat = reportInput.getReportFormat();
|
||||
if(reportFormat == null)
|
||||
{
|
||||
throw new QException("Report format was not specified.");
|
||||
}
|
||||
reportStreamer = reportFormat.newReportStreamer();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -179,6 +190,17 @@ public class GenerateReportAction
|
||||
}
|
||||
|
||||
outputSummaries(reportInput);
|
||||
|
||||
reportStreamer.finish();
|
||||
|
||||
try
|
||||
{
|
||||
reportInput.getReportOutputStream().close();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error completing report", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -193,48 +215,45 @@ public class GenerateReportAction
|
||||
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
|
||||
variableInterpreter.addValueMap("input", reportInput.getInputValues());
|
||||
|
||||
ExportInput exportInput = new ExportInput(reportInput.getInstance());
|
||||
exportInput.setSession(reportInput.getSession());
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
|
||||
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
|
||||
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
|
||||
|
||||
JoinsContext joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins());
|
||||
|
||||
List<QFieldMetaData> fields;
|
||||
if(CollectionUtils.nullSafeHasContents(reportView.getColumns()))
|
||||
JoinsContext joinsContext = null;
|
||||
if(StringUtils.hasContent(dataSource.getSourceTable()))
|
||||
{
|
||||
fields = new ArrayList<>();
|
||||
for(QReportField column : reportView.getColumns())
|
||||
{
|
||||
if(column.getIsVirtual())
|
||||
{
|
||||
fields.add(column.toField());
|
||||
}
|
||||
else
|
||||
{
|
||||
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext.getFieldAndTableNameOrAlias(column.getName());
|
||||
if(fieldAndTableNameOrAlias.field() == null)
|
||||
{
|
||||
throw new QReportingException("Could not find field named [" + column.getName() + "] on table [" + table.getName() + "]");
|
||||
}
|
||||
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
|
||||
}
|
||||
|
||||
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
|
||||
field.setName(column.getName());
|
||||
if(StringUtils.hasContent(column.getLabel()))
|
||||
{
|
||||
field.setLabel(column.getLabel());
|
||||
}
|
||||
fields.add(field);
|
||||
List<QFieldMetaData> fields = new ArrayList<>();
|
||||
for(QReportField column : reportView.getColumns())
|
||||
{
|
||||
if(column.getIsVirtual())
|
||||
{
|
||||
fields.add(column.toField());
|
||||
}
|
||||
else
|
||||
{
|
||||
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
|
||||
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext == null ? null : joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName);
|
||||
if(fieldAndTableNameOrAlias == null || fieldAndTableNameOrAlias.field() == null)
|
||||
{
|
||||
throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + dataSource.getName() + "]");
|
||||
}
|
||||
|
||||
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
|
||||
field.setName(column.getName());
|
||||
if(StringUtils.hasContent(column.getLabel()))
|
||||
{
|
||||
field.setLabel(column.getLabel());
|
||||
}
|
||||
fields.add(field);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
fields = new ArrayList<>(table.getFields().values());
|
||||
}
|
||||
|
||||
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
|
||||
reportStreamer.start(exportInput, fields, reportView.getLabel());
|
||||
}
|
||||
@ -256,8 +275,7 @@ public class GenerateReportAction
|
||||
{
|
||||
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
|
||||
|
||||
transformStepInput = new RunBackendStepInput(reportInput.getInstance());
|
||||
transformStepInput.setSession(reportInput.getSession());
|
||||
transformStepInput = new RunBackendStepInput();
|
||||
transformStepInput.setValues(reportInput.getInputValues());
|
||||
|
||||
transformStepOutput = new RunBackendStepOutput();
|
||||
@ -275,21 +293,29 @@ public class GenerateReportAction
|
||||
/////////////////////////////////////////////////////////////////
|
||||
// run a record pipe loop, over the query for this data source //
|
||||
/////////////////////////////////////////////////////////////////
|
||||
RecordPipe recordPipe = new RecordPipe();
|
||||
RecordPipe recordPipe = new BufferedRecordPipe(1000);
|
||||
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
|
||||
{
|
||||
if(dataSource.getSourceTable() != null)
|
||||
{
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter().clone();
|
||||
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
|
||||
setInputValuesInQueryFilter(reportInput, queryFilter);
|
||||
|
||||
QueryInput queryInput = new QueryInput(reportInput.getInstance());
|
||||
queryInput.setSession(reportInput.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setRecordPipe(recordPipe);
|
||||
queryInput.setTableName(dataSource.getSourceTable());
|
||||
queryInput.setFilter(queryFilter);
|
||||
queryInput.setQueryJoins(dataSource.getQueryJoins());
|
||||
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this?
|
||||
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
|
||||
|
||||
if(dataSource.getQueryInputCustomizer() != null)
|
||||
{
|
||||
DataSourceQueryInputCustomizer queryInputCustomizer = QCodeLoader.getAdHoc(DataSourceQueryInputCustomizer.class, dataSource.getQueryInputCustomizer());
|
||||
queryInput = queryInputCustomizer.run(reportInput, queryInput);
|
||||
}
|
||||
|
||||
return (new QueryAction().execute(queryInput));
|
||||
}
|
||||
else if(dataSource.getStaticDataSupplier() != null)
|
||||
@ -337,6 +363,45 @@ public class GenerateReportAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
|
||||
{
|
||||
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
|
||||
|
||||
for(QReportView view : report.getViews())
|
||||
{
|
||||
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if this is a column marked as ShowPossibleValueLabel, then we need to translate it //
|
||||
////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(column.getShowPossibleValueLabel())
|
||||
{
|
||||
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
|
||||
fieldsToTranslatePossibleValues.add(effectiveFieldName);
|
||||
}
|
||||
}
|
||||
|
||||
for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// all pivotFields that are possible value sources are implicitly translated //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
{
|
||||
fieldsToTranslatePossibleValues.add(summaryField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (fieldsToTranslatePossibleValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -358,7 +423,7 @@ public class GenerateReportAction
|
||||
for(Serializable value : criterion.getValues())
|
||||
{
|
||||
String valueAsString = ValueUtils.getValueAsString(value);
|
||||
Serializable interpretedValue = variableInterpreter.interpret(valueAsString);
|
||||
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
|
||||
newValues.add(interpretedValue);
|
||||
}
|
||||
criterion.setValues(newValues);
|
||||
@ -380,6 +445,22 @@ public class GenerateReportAction
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
if(tableView != null)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QReportField column : tableView.getColumns())
|
||||
{
|
||||
if(column.getShowPossibleValueLabel())
|
||||
{
|
||||
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
|
||||
for(QRecord record : records)
|
||||
{
|
||||
String displayValue = record.getDisplayValue(effectiveFieldName);
|
||||
record.setValue(column.getName(), displayValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reportStreamer.addRecords(records);
|
||||
}
|
||||
|
||||
@ -441,6 +522,9 @@ public class GenerateReportAction
|
||||
Serializable summaryValue = record.getValue(summaryField);
|
||||
if(table.getField(summaryField).getPossibleValueSourceName() != null)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// so, this is kinda a thing - where we implicitly use possible-value labels (e.g., display values) for pivot fields... //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
summaryValue = record.getDisplayValue(summaryField);
|
||||
}
|
||||
key.add(summaryField, summaryValue);
|
||||
@ -509,8 +593,7 @@ public class GenerateReportAction
|
||||
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
|
||||
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
|
||||
|
||||
ExportInput exportInput = new ExportInput(reportInput.getInstance());
|
||||
exportInput.setSession(reportInput.getSession());
|
||||
ExportInput exportInput = new ExportInput();
|
||||
exportInput.setReportFormat(reportFormat);
|
||||
exportInput.setFilename(reportInput.getFilename());
|
||||
exportInput.setTitleRow(summaryOutput.titleRow);
|
||||
@ -527,8 +610,6 @@ public class GenerateReportAction
|
||||
reportStreamer.addTotalsRow(summaryOutput.totalRow);
|
||||
}
|
||||
}
|
||||
|
||||
reportStreamer.finish();
|
||||
}
|
||||
|
||||
|
||||
|
@ -0,0 +1,177 @@
|
||||
/*
|
||||
* 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.reporting;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.utils.JsonUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** JSON export format implementation
|
||||
*******************************************************************************/
|
||||
public class JsonExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(JsonExportStreamer.class);
|
||||
|
||||
private ExportInput exportInput;
|
||||
private QTableMetaData table;
|
||||
private List<QFieldMetaData> fields;
|
||||
private OutputStream outputStream;
|
||||
|
||||
private boolean needComma = false;
|
||||
private boolean prettyPrint = true;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public JsonExportStreamer()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
|
||||
{
|
||||
this.exportInput = exportInput;
|
||||
this.fields = fields;
|
||||
table = exportInput.getTable();
|
||||
outputStream = this.exportInput.getReportOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
outputStream.write('[');
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw (new QReportingException("Error starting report output", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addRecords(List<QRecord> qRecords) throws QReportingException
|
||||
{
|
||||
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
|
||||
|
||||
for(QRecord qRecord : qRecords)
|
||||
{
|
||||
writeRecord(qRecord);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void writeRecord(QRecord qRecord) throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
if(needComma)
|
||||
{
|
||||
outputStream.write(',');
|
||||
}
|
||||
|
||||
Map<String, Serializable> mapForJson = new LinkedHashMap<>();
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", ""));
|
||||
mapForJson.put(labelForJson, qRecord.getValue(field.getName()));
|
||||
}
|
||||
|
||||
String json = prettyPrint ? JsonUtils.toPrettyJson(mapForJson) : JsonUtils.toJson(mapForJson);
|
||||
|
||||
if(prettyPrint)
|
||||
{
|
||||
outputStream.write('\n');
|
||||
}
|
||||
|
||||
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
outputStream.flush(); // todo - less often?
|
||||
needComma = true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
throw (new QReportingException("Error writing JSON report", e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void addTotalsRow(QRecord record) throws QReportingException
|
||||
{
|
||||
writeRecord(record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void finish() throws QReportingException
|
||||
{
|
||||
try
|
||||
{
|
||||
if(prettyPrint)
|
||||
{
|
||||
outputStream.write('\n');
|
||||
}
|
||||
outputStream.write(']');
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
throw (new QReportingException("Error ending report output", e));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -27,11 +27,10 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -41,7 +40,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class ListOfMapsExportStreamer implements ExportStreamerInterface
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(ListOfMapsExportStreamer.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(ListOfMapsExportStreamer.class);
|
||||
|
||||
private ExportInput exportInput;
|
||||
private List<QFieldMetaData> fields;
|
||||
|
@ -27,10 +27,9 @@ import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -39,7 +38,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class RecordPipe
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(RecordPipe.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(RecordPipe.class);
|
||||
|
||||
private static final long BLOCKING_SLEEP_MILLIS = 100;
|
||||
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
|
||||
|
@ -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.actions.reporting.customizers;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Interface for customizer on a QReportDataSource's query.
|
||||
**
|
||||
** Useful, for example, to look at what input field values were given, and change
|
||||
** the query filter (e.g., conditional criteria), or issue an error based on the
|
||||
** combination of input fields given.
|
||||
*******************************************************************************/
|
||||
public interface DataSourceQueryInputCustomizer
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
QueryInput run(ReportInput reportInput, QueryInput queryInput) throws QException;
|
||||
|
||||
}
|
@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
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.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
@ -35,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptI
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
|
||||
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.Script;
|
||||
@ -46,6 +51,8 @@ import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
|
||||
*******************************************************************************/
|
||||
public class RunAssociatedScriptAction
|
||||
{
|
||||
private Map<AssociatedScriptCodeReference, ScriptRevision> scriptRevisionCache = new HashMap<>();
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -54,32 +61,37 @@ public class RunAssociatedScriptAction
|
||||
{
|
||||
ActionHelper.validateSession(input);
|
||||
|
||||
Serializable scriptId = getScriptId(input);
|
||||
if(scriptId == null)
|
||||
{
|
||||
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
|
||||
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
|
||||
}
|
||||
ScriptRevision scriptRevision = getScriptRevision(input);
|
||||
|
||||
Script script = getScript(input, scriptId);
|
||||
if(script.getCurrentScriptRevisionId() == null)
|
||||
{
|
||||
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
|
||||
+ "] (scriptId=" + scriptId + ") does not have a current version."));
|
||||
}
|
||||
|
||||
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
|
||||
|
||||
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
|
||||
executeCodeInput.setSession(input.getSession());
|
||||
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
|
||||
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
|
||||
executeCodeInput.setContext(new HashMap<>());
|
||||
if(input.getOutputObject() != null)
|
||||
{
|
||||
executeCodeInput.getContext().put("output", input.getOutputObject());
|
||||
}
|
||||
|
||||
if(input.getScriptUtils() != null)
|
||||
{
|
||||
executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils());
|
||||
}
|
||||
|
||||
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
|
||||
executeCodeInput.setExecutionLogger(new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
|
||||
executeCodeInput.setExecutionLogger(executionLogger);
|
||||
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
|
||||
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
|
||||
}
|
||||
|
||||
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
|
||||
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
|
||||
|
||||
@ -88,13 +100,42 @@ public class RunAssociatedScriptAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private ScriptRevision getScriptRevision(RunAssociatedScriptInput input) throws QException
|
||||
{
|
||||
if(!scriptRevisionCache.containsKey(input.getCodeReference()))
|
||||
{
|
||||
Serializable scriptId = getScriptId(input);
|
||||
if(scriptId == null)
|
||||
{
|
||||
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
|
||||
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
|
||||
}
|
||||
|
||||
Script script = getScript(input, scriptId);
|
||||
if(script.getCurrentScriptRevisionId() == null)
|
||||
{
|
||||
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
|
||||
+ "] (scriptId=" + scriptId + ") does not have a current version."));
|
||||
}
|
||||
|
||||
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
|
||||
scriptRevisionCache.put(input.getCodeReference(), scriptRevision);
|
||||
}
|
||||
|
||||
return scriptRevisionCache.get(input.getCodeReference());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException
|
||||
{
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName("scriptRevision");
|
||||
getInput.setPrimaryKey(scriptRevisionId);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
@ -114,8 +155,7 @@ public class RunAssociatedScriptAction
|
||||
*******************************************************************************/
|
||||
private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException
|
||||
{
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName("script");
|
||||
getInput.setPrimaryKey(scriptId);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
@ -136,8 +176,7 @@ public class RunAssociatedScriptAction
|
||||
*******************************************************************************/
|
||||
private Serializable getScriptId(RunAssociatedScriptInput input) throws QException
|
||||
{
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(input.getCodeReference().getRecordTable());
|
||||
getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey());
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
|
@ -83,8 +83,7 @@ public class StoreAssociatedScriptAction
|
||||
/////////////////////////////////////////////////////////////
|
||||
QRecord associatedRecord;
|
||||
{
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName(input.getTableName());
|
||||
getInput.setPrimaryKey(input.getRecordPrimaryKey());
|
||||
getInput.setShouldGenerateDisplayValues(true);
|
||||
@ -107,8 +106,7 @@ public class StoreAssociatedScriptAction
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// get the script type - that'll be part of the new script's name //
|
||||
////////////////////////////////////////////////////////////////////
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName("scriptType");
|
||||
getInput.setPrimaryKey(associatedScript.getScriptTypeId());
|
||||
getInput.setShouldGenerateDisplayValues(true);
|
||||
@ -125,8 +123,7 @@ public class StoreAssociatedScriptAction
|
||||
script = new QRecord();
|
||||
script.setValue("scriptTypeId", associatedScript.getScriptTypeId());
|
||||
script.setValue("name", associatedRecord.getRecordLabel() + " - " + scriptType.getRecordLabel());
|
||||
InsertInput insertInput = new InsertInput(input.getInstance());
|
||||
insertInput.setSession(input.getSession());
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("script");
|
||||
insertInput.setRecords(List.of(script));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
@ -135,8 +132,7 @@ public class StoreAssociatedScriptAction
|
||||
/////////////////////////////////////////////////////////////
|
||||
// update the associated record to point at the new script //
|
||||
/////////////////////////////////////////////////////////////
|
||||
UpdateInput updateInput = new UpdateInput(input.getInstance());
|
||||
updateInput.setSession(input.getSession());
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(input.getTableName());
|
||||
updateInput.setRecords(List.of(new QRecord()
|
||||
.withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField()))
|
||||
@ -149,15 +145,13 @@ public class StoreAssociatedScriptAction
|
||||
////////////////////////////////////////
|
||||
// get the existing script, to update //
|
||||
////////////////////////////////////////
|
||||
GetInput getInput = new GetInput(input.getInstance());
|
||||
getInput.setSession(input.getSession());
|
||||
GetInput getInput = new GetInput();
|
||||
getInput.setTableName("script");
|
||||
getInput.setPrimaryKey(existingScriptId);
|
||||
GetOutput getOutput = new GetAction().execute(getInput);
|
||||
script = getOutput.getRecord();
|
||||
|
||||
QueryInput queryInput = new QueryInput(input.getInstance());
|
||||
queryInput.setSession(input.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName("scriptRevision");
|
||||
queryInput.setFilter(new QQueryFilter()
|
||||
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
|
||||
@ -202,8 +196,7 @@ public class StoreAssociatedScriptAction
|
||||
scriptRevision.setValue("author", "Unknown");
|
||||
}
|
||||
|
||||
InsertInput insertInput = new InsertInput(input.getInstance());
|
||||
insertInput.setSession(input.getSession());
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("scriptRevision");
|
||||
insertInput.setRecords(List.of(scriptRevision));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
@ -213,8 +206,7 @@ public class StoreAssociatedScriptAction
|
||||
// update the script to point at the new revision //
|
||||
////////////////////////////////////////////////////
|
||||
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
|
||||
UpdateInput updateInput = new UpdateInput(input.getInstance());
|
||||
updateInput.setSession(input.getSession());
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName("script");
|
||||
updateInput.setRecords(List.of(script));
|
||||
new UpdateAction().execute(updateInput);
|
||||
|
@ -80,8 +80,7 @@ public interface TestScriptActionInterface
|
||||
*******************************************************************************/
|
||||
default void execute(TestScriptInput input, TestScriptOutput output) throws QException
|
||||
{
|
||||
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
|
||||
executeCodeInput.setSession(input.getSession());
|
||||
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
|
||||
executeCodeInput.setContext(new HashMap<>());
|
||||
|
||||
executeCodeInput.setCodeReference(input.getCodeReference());
|
||||
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger implements ScriptExecutionLoggerInterface
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger.class);
|
||||
|
||||
private List<QRecord> scriptLogs = new ArrayList<>();
|
||||
private List<List<QRecord>> scriptLogLines = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
|
||||
{
|
||||
super.acceptExecutionStart(executeCodeInput);
|
||||
super.setScriptLogLines(new ArrayList<>());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptException(Exception exception)
|
||||
{
|
||||
accumulate(null, exception);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void acceptExecutionEnd(Serializable output)
|
||||
{
|
||||
accumulate(output, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void accumulate(Serializable output, Exception exception)
|
||||
{
|
||||
super.updateHeaderAtEnd(output, exception);
|
||||
scriptLogs.add(super.getScriptLog());
|
||||
scriptLogLines.add(new ArrayList<>(super.getScriptLogLines()));
|
||||
super.getScriptLogLines().clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void storeAndClear()
|
||||
{
|
||||
try
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("scriptLog");
|
||||
insertInput.setRecords(scriptLogs);
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
|
||||
List<QRecord> flatScriptLogLines = new ArrayList<>();
|
||||
for(int i = 0; i < insertOutput.getRecords().size(); i++)
|
||||
{
|
||||
QRecord insertedScriptLog = insertOutput.getRecords().get(i);
|
||||
List<QRecord> subScriptLogLines = scriptLogLines.get(i);
|
||||
subScriptLogLines.forEach(r -> r.setValue("scriptLogId", insertedScriptLog.getValueInteger("id")));
|
||||
flatScriptLogLines.addAll(subScriptLogLines);
|
||||
}
|
||||
|
||||
insertInput = new InsertInput();
|
||||
insertInput.setTableName("scriptLogLine");
|
||||
insertInput.setRecords(flatScriptLogLines);
|
||||
new InsertAction().execute(insertInput);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error storing script logs", e);
|
||||
}
|
||||
|
||||
scriptLogs.clear();
|
||||
scriptLogLines.clear();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setScriptId(Integer scriptId)
|
||||
{
|
||||
super.setScriptId(scriptId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setScriptRevisionId(Integer scriptRevisionId)
|
||||
{
|
||||
super.setScriptRevisionId(scriptRevisionId);
|
||||
}
|
||||
}
|
@ -27,12 +27,11 @@ import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -40,9 +39,9 @@ import org.apache.logging.log4j.Logger;
|
||||
** scriptLogLine records - but doesn't insert them. e.g., useful for testing
|
||||
** (both in junit, and for users in-app).
|
||||
*******************************************************************************/
|
||||
public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface, ScriptExecutionLoggerInterface
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
|
||||
|
||||
private QRecord scriptLog;
|
||||
private List<QRecord> scriptLogLines = new ArrayList<>();
|
||||
@ -218,4 +217,37 @@ public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecu
|
||||
{
|
||||
this.scriptLog = scriptLog;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for scriptLogLines
|
||||
**
|
||||
*******************************************************************************/
|
||||
protected void setScriptLogLines(List<QRecord> scriptLogLines)
|
||||
{
|
||||
this.scriptLogLines = scriptLogLines;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setScriptId(Integer scriptId)
|
||||
{
|
||||
this.scriptId = scriptId;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public void setScriptRevisionId(Integer scriptRevisionId)
|
||||
{
|
||||
this.scriptRevisionId = scriptRevisionId;
|
||||
}
|
||||
}
|
||||
|
@ -24,12 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.scripts.logging;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -37,7 +36,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(Log4jCodeExecutionLogger.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(Log4jCodeExecutionLogger.class);
|
||||
|
||||
private QCodeReference qCodeReference;
|
||||
private String uuid = UUID.randomUUID().toString();
|
||||
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public interface ScriptExecutionLoggerInterface
|
||||
{
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void setScriptId(Integer scriptId);
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
void setScriptRevisionId(Integer scriptRevisionId);
|
||||
}
|
@ -26,13 +26,12 @@ import java.io.Serializable;
|
||||
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.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
|
||||
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;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -41,7 +40,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
|
||||
|
||||
|
||||
|
||||
@ -66,8 +65,7 @@ public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLo
|
||||
{
|
||||
super.acceptExecutionStart(executeCodeInput);
|
||||
|
||||
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
|
||||
insertInput.setSession(executeCodeInput.getSession());
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("scriptLog");
|
||||
insertInput.setRecords(List.of(getScriptLog()));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
@ -112,16 +110,14 @@ public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLo
|
||||
try
|
||||
{
|
||||
updateHeaderAtEnd(output, exception);
|
||||
UpdateInput updateInput = new UpdateInput(executeCodeInput.getInstance());
|
||||
updateInput.setSession(executeCodeInput.getSession());
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName("scriptLog");
|
||||
updateInput.setRecords(List.of(getScriptLog()));
|
||||
new UpdateAction().execute(updateInput);
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(getScriptLogLines()))
|
||||
{
|
||||
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
|
||||
insertInput.setSession(executeCodeInput.getSession());
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName("scriptLogLine");
|
||||
insertInput.setRecords(getScriptLogLines());
|
||||
new InsertAction().execute(insertInput);
|
||||
|
@ -28,6 +28,7 @@ import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
|
||||
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.query.QueryInput;
|
||||
@ -35,8 +36,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -45,7 +44,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class DeleteAction
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(DeleteAction.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
|
||||
|
||||
|
||||
|
||||
@ -102,7 +101,7 @@ public class DeleteAction
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
|
||||
|
||||
QueryInput queryInput = new QueryInput(deleteInput.getInstance(), deleteInput.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(deleteInput.getTableName());
|
||||
queryInput.setFilter(deleteInput.getQueryFilter());
|
||||
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
|
||||
|
@ -22,26 +22,40 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
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.GetInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -50,10 +64,9 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
*******************************************************************************/
|
||||
public class GetAction
|
||||
{
|
||||
private Optional<Function<QRecord, QRecord>> postGetRecordCustomizer;
|
||||
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
|
||||
|
||||
private GetInput getInput;
|
||||
private QValueFormatter qValueFormatter;
|
||||
private QPossibleValueTranslator qPossibleValueTranslator;
|
||||
|
||||
|
||||
@ -65,7 +78,13 @@ public class GetAction
|
||||
{
|
||||
ActionHelper.validateSession(getInput);
|
||||
|
||||
postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
QTableMetaData table = getInput.getTable();
|
||||
if(table == null)
|
||||
{
|
||||
throw (new QException("Requested to Get a record from an unrecognized table: " + getInput.getTableName()));
|
||||
}
|
||||
|
||||
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
this.getInput = getInput;
|
||||
|
||||
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
|
||||
@ -79,22 +98,54 @@ public class GetAction
|
||||
}
|
||||
catch(IllegalStateException ise)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if a module doesn't implement Get directly - try to do a Get by a Query by the primary key //
|
||||
// see below. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if a module doesn't implement Get directly - try to do a Get by a Query in the DefaultGetInterface (inner class) //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
}
|
||||
|
||||
GetOutput getOutput;
|
||||
if(getInterface != null)
|
||||
if(getInterface == null)
|
||||
{
|
||||
getOutput = getInterface.execute(getInput);
|
||||
}
|
||||
else
|
||||
{
|
||||
getOutput = performGetViaQuery(getInput);
|
||||
getInterface = new DefaultGetInterface();
|
||||
}
|
||||
|
||||
getInterface.validateInput(getInput);
|
||||
getOutput = getInterface.execute(getInput);
|
||||
|
||||
////////////////////////////
|
||||
// handle cache use-cases //
|
||||
////////////////////////////
|
||||
if(table.getCacheOf() != null)
|
||||
{
|
||||
if(getOutput.getRecord() == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// if the record wasn't found, see if we should look in cache-source //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
|
||||
if(recordFromSource != null)
|
||||
{
|
||||
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
|
||||
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(getInput.getTableName());
|
||||
insertInput.setRecords(List.of(recordToCache));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
getOutput.setRecord(insertOutput.getRecords().get(0));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// if the record was found, but it's too old, maybe re-fetch from cache source //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
refreshCacheIfExpired(getInput, getOutput);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
// if the record is found, perform post-actions on it //
|
||||
////////////////////////////////////////////////////////
|
||||
if(getOutput.getRecord() != null)
|
||||
{
|
||||
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
|
||||
@ -108,20 +159,159 @@ public class GetAction
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private GetOutput performGetViaQuery(GetInput getInput) throws QException
|
||||
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(getInput.getInstance());
|
||||
queryInput.setSession(getInput.getSession());
|
||||
queryInput.setTableName(getInput.getTableName());
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey()))));
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
GetOutput getOutput = new GetOutput();
|
||||
if(!queryOutput.getRecords().isEmpty())
|
||||
QRecord cacheRecord = new QRecord(recordFromSource);
|
||||
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
|
||||
{
|
||||
getOutput.setRecord(queryOutput.getRecords().get(0));
|
||||
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
|
||||
}
|
||||
return (cacheRecord);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
|
||||
{
|
||||
QTableMetaData table = getInput.getTable();
|
||||
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
|
||||
if(expirationSeconds != null)
|
||||
{
|
||||
QRecord cachedRecord = getOutput.getRecord();
|
||||
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
|
||||
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
|
||||
{
|
||||
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
|
||||
if(recordFromSource != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// if the record was found in the source, update it in the cache //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
|
||||
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
|
||||
|
||||
UpdateInput updateInput = new UpdateInput();
|
||||
updateInput.setTableName(getInput.getTableName());
|
||||
updateInput.setRecords(List.of(recordToCache));
|
||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||
|
||||
getOutput.setRecord(updateOutput.getRecords().get(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// if the record is no longer in the source, then remove it from the cache //
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
DeleteInput deleteInput = new DeleteInput();
|
||||
deleteInput.setTableName(getInput.getTableName());
|
||||
deleteInput.setPrimaryKeys(List.of(getOutput.getRecord().getValue(table.getPrimaryKeyField())));
|
||||
new DeleteAction().execute(deleteInput);
|
||||
|
||||
getOutput.setRecord(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QRecord tryToGetFromCacheSource(GetInput getInput, GetOutput getOutput) throws QException
|
||||
{
|
||||
QRecord recordFromSource = null;
|
||||
QTableMetaData table = getInput.getTable();
|
||||
|
||||
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
|
||||
{
|
||||
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
|
||||
{
|
||||
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo!!
|
||||
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
|
||||
}
|
||||
}
|
||||
|
||||
return (recordFromSource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
|
||||
{
|
||||
/////////////////////////////////////////////////////
|
||||
// do a Get on the source table, by the unique key //
|
||||
/////////////////////////////////////////////////////
|
||||
GetInput sourceGetInput = new GetInput();
|
||||
sourceGetInput.setTableName(sourceTableName);
|
||||
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
|
||||
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
|
||||
return (sourceGetOutput.getRecord());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static class DefaultGetInterface implements GetInterface
|
||||
{
|
||||
@Override
|
||||
public GetOutput execute(GetInput getInput) throws QException
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
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.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
GetOutput getOutput = new GetOutput();
|
||||
if(!queryOutput.getRecords().isEmpty())
|
||||
{
|
||||
getOutput.setRecord(queryOutput.getRecords().get(0));
|
||||
}
|
||||
return (getOutput);
|
||||
}
|
||||
return (getOutput);
|
||||
}
|
||||
|
||||
|
||||
@ -135,7 +325,7 @@ public class GetAction
|
||||
QRecord returnRecord = record;
|
||||
if(this.postGetRecordCustomizer.isPresent())
|
||||
{
|
||||
returnRecord = postGetRecordCustomizer.get().apply(record);
|
||||
returnRecord = postGetRecordCustomizer.get().apply(List.of(record)).get(0);
|
||||
}
|
||||
|
||||
if(getInput.getShouldTranslatePossibleValues())
|
||||
@ -149,13 +339,13 @@ public class GetAction
|
||||
|
||||
if(getInput.getShouldGenerateDisplayValues())
|
||||
{
|
||||
if(qValueFormatter == null)
|
||||
{
|
||||
qValueFormatter = new QValueFormatter();
|
||||
}
|
||||
qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
|
||||
QValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// note - shouldFetchHeavyFields should be handled by the underlying action //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
return (returnRecord);
|
||||
}
|
||||
}
|
||||
|
@ -22,19 +22,33 @@
|
||||
package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
|
||||
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
|
||||
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.tables.helpers.UniqueKeyHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
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.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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -43,7 +57,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOutput>
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(InsertAction.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
|
||||
|
||||
|
||||
|
||||
@ -54,20 +68,96 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
public InsertOutput execute(InsertInput insertInput) throws QException
|
||||
{
|
||||
ActionHelper.validateSession(insertInput);
|
||||
QTableMetaData table = insertInput.getTable();
|
||||
|
||||
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
|
||||
setAutomationStatusField(insertInput);
|
||||
|
||||
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), insertInput.getTable(), insertInput.getRecords());
|
||||
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
|
||||
// todo - need to handle records with errors coming out of here...
|
||||
|
||||
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
|
||||
// todo pre-customization - just get to modify the request?
|
||||
|
||||
setErrorsIfUniqueKeyErrors(insertInput, table);
|
||||
|
||||
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
if(postInsertCustomizer.isPresent())
|
||||
{
|
||||
postInsertCustomizer.get().setInsertInput(insertInput);
|
||||
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
|
||||
}
|
||||
|
||||
return insertOutput;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void setErrorsIfUniqueKeyErrors(InsertInput insertInput, QTableMetaData table) throws QException
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(table.getUniqueKeys()))
|
||||
{
|
||||
Map<UniqueKey, Set<List<Serializable>>> keysInThisList = new HashMap<>();
|
||||
if(insertInput.getSkipUniqueKeyCheck())
|
||||
{
|
||||
LOG.debug("Skipping unique key check in " + insertInput.getTableName() + " insert.");
|
||||
return;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
// check for any pre-existing unique keys //
|
||||
////////////////////////////////////////////
|
||||
Map<UniqueKey, Set<List<Serializable>>> existingKeys = new HashMap<>();
|
||||
List<UniqueKey> uniqueKeys = CollectionUtils.nonNullList(table.getUniqueKeys());
|
||||
for(UniqueKey uniqueKey : uniqueKeys)
|
||||
{
|
||||
existingKeys.put(uniqueKey, UniqueKeyHelper.getExistingKeys(insertInput, insertInput.getTransaction(), table, insertInput.getRecords(), uniqueKey));
|
||||
}
|
||||
|
||||
/////////////////////////////////////
|
||||
// make sure this map is populated //
|
||||
/////////////////////////////////////
|
||||
uniqueKeys.forEach(uk -> keysInThisList.computeIfAbsent(uk, x -> new HashSet<>()));
|
||||
|
||||
for(QRecord record : insertInput.getRecords())
|
||||
{
|
||||
//////////////////////////////////////////////////////////
|
||||
// check if this record violates any of the unique keys //
|
||||
//////////////////////////////////////////////////////////
|
||||
boolean foundDupe = false;
|
||||
for(UniqueKey uniqueKey : uniqueKeys)
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
||||
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
|
||||
{
|
||||
record.addError("Another record already exists with this " + uniqueKey.getDescription(table));
|
||||
foundDupe = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// if this record doesn't violate any uk's, then we can add it to the output //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
if(!foundDupe)
|
||||
{
|
||||
for(UniqueKey uniqueKey : uniqueKeys)
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
||||
keyValues.ifPresent(kv -> keysInThisList.get(uniqueKey).add(kv));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** If the table being inserted into uses an automation-status field, populate it now.
|
||||
*******************************************************************************/
|
||||
|
@ -24,13 +24,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
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.reporting.BufferedRecordPipe;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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;
|
||||
@ -44,10 +46,11 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
*******************************************************************************/
|
||||
public class QueryAction
|
||||
{
|
||||
private Optional<Function<QRecord, QRecord>> postQueryRecordCustomizer;
|
||||
private static final QLogger LOG = QLogger.getLogger(QueryAction.class);
|
||||
|
||||
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
|
||||
|
||||
private QueryInput queryInput;
|
||||
private QValueFormatter qValueFormatter;
|
||||
private QPossibleValueTranslator qPossibleValueTranslator;
|
||||
|
||||
|
||||
@ -59,7 +62,7 @@ public class QueryAction
|
||||
{
|
||||
ActionHelper.validateSession(queryInput);
|
||||
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
|
||||
this.queryInput = queryInput;
|
||||
|
||||
if(queryInput.getRecordPipe() != null)
|
||||
@ -73,6 +76,11 @@ public class QueryAction
|
||||
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
|
||||
// todo post-customization - can do whatever w/ the result if you want
|
||||
|
||||
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
|
||||
{
|
||||
bufferedRecordPipe.finalFlush();
|
||||
}
|
||||
|
||||
if(queryInput.getRecordPipe() == null)
|
||||
{
|
||||
postRecordActions(queryOutput.getRecords());
|
||||
@ -92,7 +100,7 @@ public class QueryAction
|
||||
{
|
||||
if(this.postQueryRecordCustomizer.isPresent())
|
||||
{
|
||||
records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t));
|
||||
records = postQueryRecordCustomizer.get().apply(records);
|
||||
}
|
||||
|
||||
if(queryInput.getShouldTranslatePossibleValues())
|
||||
@ -101,16 +109,12 @@ public class QueryAction
|
||||
{
|
||||
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
|
||||
}
|
||||
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records);
|
||||
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins(), queryInput.getFieldsToTranslatePossibleValues());
|
||||
}
|
||||
|
||||
if(queryInput.getShouldGenerateDisplayValues())
|
||||
{
|
||||
if(qValueFormatter == null)
|
||||
{
|
||||
qValueFormatter = new QValueFormatter();
|
||||
}
|
||||
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
|
||||
QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.tables.helpers;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.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.fields.QFieldMetaData;
|
||||
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;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Methods to help with unique key checks.
|
||||
*******************************************************************************/
|
||||
public class UniqueKeyHelper
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Set<List<Serializable>> getExistingKeys(AbstractActionInput actionInput, QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
|
||||
{
|
||||
List<String> ukFieldNames = uniqueKey.getFieldNames();
|
||||
Set<List<Serializable>> existingRecords = new HashSet<>();
|
||||
if(ukFieldNames != null)
|
||||
{
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(table.getName());
|
||||
queryInput.setTransaction(transaction);
|
||||
|
||||
QQueryFilter filter = new QQueryFilter();
|
||||
if(ukFieldNames.size() == 1)
|
||||
{
|
||||
List<Serializable> values = recordList.stream()
|
||||
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors()))
|
||||
.map(r -> r.getValue(ukFieldNames.get(0)))
|
||||
.collect(Collectors.toList());
|
||||
filter.addCriteria(new QFilterCriteria(ukFieldNames.get(0), QCriteriaOperator.IN, values));
|
||||
}
|
||||
else
|
||||
{
|
||||
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
for(QRecord record : recordList)
|
||||
{
|
||||
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
filter.addSubFilter(subFilter);
|
||||
for(String fieldName : ukFieldNames)
|
||||
{
|
||||
Serializable value = record.getValue(fieldName);
|
||||
if(value == null)
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
|
||||
}
|
||||
else
|
||||
{
|
||||
subFilter.addCriteria(new QFilterCriteria(fieldName, QCriteriaOperator.EQUALS, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeIsEmpty(filter.getSubFilters()))
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if we didn't build any sub-filters (because all records have errors in them), don't run a query w/ no clauses - rather - return early. //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (existingRecords);
|
||||
}
|
||||
}
|
||||
|
||||
queryInput.setFilter(filter);
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
for(QRecord record : queryOutput.getRecords())
|
||||
{
|
||||
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record);
|
||||
keyValues.ifPresent(existingRecords::add);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (existingRecords);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<Serializable> keyValues = new ArrayList<>();
|
||||
for(String fieldName : uniqueKey.getFieldNames())
|
||||
{
|
||||
QFieldMetaData field = table.getField(fieldName);
|
||||
Serializable value = record.getValue(fieldName);
|
||||
Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value);
|
||||
keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue);
|
||||
}
|
||||
return (Optional.of(keyValues));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return (Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** To make a list of unique key values here behave like they do in an RDBMS
|
||||
** (which is what we're trying to mimic - which is - 2 null values in a field
|
||||
** aren't considered the same, so they don't violate a unique key) (at least, that's
|
||||
** how some RDBMS's work, right??) - use this value instead of nulls in the
|
||||
** output of getKeyValues - where interestingly, this class always returns
|
||||
** false in it equals method... Unclear how bad this is, e.g., if it's violating
|
||||
** the contract for equals and hashCode...
|
||||
*******************************************************************************/
|
||||
public static class NullUniqueKeyValue implements Serializable
|
||||
{
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -82,8 +82,7 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
|
||||
*******************************************************************************/
|
||||
public static String render(AbstractActionInput parentActionInput, TemplateType templateType, Map<String, Object> context, String code) throws QException
|
||||
{
|
||||
RenderTemplateInput renderTemplateInput = new RenderTemplateInput(parentActionInput.getInstance());
|
||||
renderTemplateInput.setSession(parentActionInput.getSession());
|
||||
RenderTemplateInput renderTemplateInput = new RenderTemplateInput();
|
||||
renderTemplateInput.setCode(code);
|
||||
renderTemplateInput.setContext(context);
|
||||
renderTemplateInput.setTemplateType(templateType);
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.values;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -34,10 +35,12 @@ import java.util.Set;
|
||||
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.QueryJoin;
|
||||
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.QInstance;
|
||||
@ -50,9 +53,9 @@ 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.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.Pair;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -61,7 +64,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class QPossibleValueTranslator
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QPossibleValueTranslator.class);
|
||||
|
||||
private final QInstance qInstance;
|
||||
private final QSession session;
|
||||
@ -72,6 +75,8 @@ public class QPossibleValueTranslator
|
||||
///////////////////////////////////////////////////////
|
||||
private Map<String, Map<Serializable, String>> possibleValueCache;
|
||||
|
||||
// todo not commit - remove instance & session - use Context
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -91,13 +96,30 @@ public class QPossibleValueTranslator
|
||||
** For a list of records, translate their possible values (populating their display values)
|
||||
*******************************************************************************/
|
||||
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records)
|
||||
{
|
||||
translatePossibleValuesInRecords(table, records, Collections.emptyList(), null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of records, translate their possible values (populating their display values)
|
||||
*******************************************************************************/
|
||||
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins, Set<String> limitedToFieldNames)
|
||||
{
|
||||
if(records == null || table == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
primePvsCache(table, records);
|
||||
if(limitedToFieldNames != null && limitedToFieldNames.isEmpty())
|
||||
{
|
||||
LOG.debug("We were asked to translate possible values, but then given an empty set of fields to translate, so noop.");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.debug("Translating possible values in [" + records.size() + "] records from the [" + table.getName() + "] table.");
|
||||
primePvsCache(table, records, queryJoins, limitedToFieldNames);
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
@ -105,9 +127,48 @@ public class QPossibleValueTranslator
|
||||
{
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName())));
|
||||
if(limitedToFieldNames == null || limitedToFieldNames.contains(field.getName()))
|
||||
{
|
||||
record.setDisplayValue(field.getName(), translatePossibleValue(field, record.getValue(field.getName())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
|
||||
{
|
||||
if(queryJoin.getSelect())
|
||||
{
|
||||
try
|
||||
{
|
||||
QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable());
|
||||
for(QFieldMetaData field : joinTable.getFields().values())
|
||||
{
|
||||
String joinFieldName = Objects.requireNonNullElse(queryJoin.getAlias(), joinTable.getName()) + "." + field.getName();
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
if(limitedToFieldNames == null || limitedToFieldNames.contains(joinFieldName))
|
||||
{
|
||||
///////////////////////////////////////////////
|
||||
// avoid circling-back upon the source table //
|
||||
///////////////////////////////////////////////
|
||||
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
|
||||
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && table.getName().equals(possibleValueSource.getTableName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
record.setDisplayValue(joinFieldName, translatePossibleValue(field, record.getValue(joinFieldName)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.warn("Error translating join table possible values", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -244,8 +305,7 @@ public class QPossibleValueTranslator
|
||||
//////////////////////////////////////////////////////////////
|
||||
// look for cached value - if it's missing, call the primer //
|
||||
//////////////////////////////////////////////////////////////
|
||||
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
|
||||
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
|
||||
Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
|
||||
if(!cacheForPvs.containsKey(value))
|
||||
{
|
||||
primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), List.of(value));
|
||||
@ -329,21 +389,31 @@ public class QPossibleValueTranslator
|
||||
|
||||
/*******************************************************************************
|
||||
** prime the cache (e.g., by doing bulk-queries) for table-based PVS's
|
||||
**
|
||||
** @param table the table that the records are from
|
||||
*
|
||||
* @param table the table that the records are from
|
||||
** @param records the records that have the possible value id's (e.g., foreign keys)
|
||||
* @param queryJoins joins that were used as part of the query that led to the records.
|
||||
* @param limitedToFieldNames set of names that are the only fields that get translated (null means all fields).
|
||||
*******************************************************************************/
|
||||
void primePvsCache(QTableMetaData table, List<QRecord> records)
|
||||
void primePvsCache(QTableMetaData table, List<QRecord> records, List<QueryJoin> queryJoins, Set<String> limitedToFieldNames)
|
||||
{
|
||||
ListingHash<String, QFieldMetaData> fieldsByPvsTable = new ListingHash<>();
|
||||
ListingHash<String, QPossibleValueSource> pvsesByTable = new ListingHash<>();
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this is a map of String(tableName - the PVS table) to Pair(String (either "" for main table in a query, or join-table + "."), field (from the table being selected from)) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable = new ListingHash<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// this is a map of String(tableName - the PVS table) to PossibleValueSource objects //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
ListingHash<String, QPossibleValueSource> pvsesByTable = new ListingHash<>();
|
||||
|
||||
primePvsCacheTableListingHashLoader(table, fieldsByPvsTable, pvsesByTable, "", table.getName(), limitedToFieldNames);
|
||||
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
|
||||
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
|
||||
if(queryJoin.getSelect())
|
||||
{
|
||||
fieldsByPvsTable.add(possibleValueSource.getTableName(), field);
|
||||
pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource);
|
||||
String aliasOrTableName = Objects.requireNonNullElse(queryJoin.getAlias(), queryJoin.getJoinTable());
|
||||
primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getJoinTable()), fieldsByPvsTable, pvsesByTable, aliasOrTableName + ".", queryJoin.getJoinTable(), limitedToFieldNames);
|
||||
}
|
||||
}
|
||||
|
||||
@ -352,16 +422,24 @@ public class QPossibleValueTranslator
|
||||
Set<Serializable> values = new HashSet<>();
|
||||
for(QRecord record : records)
|
||||
{
|
||||
for(QFieldMetaData field : fieldsByPvsTable.get(tableName))
|
||||
for(Pair<String, QFieldMetaData> fieldPair : fieldsByPvsTable.get(tableName))
|
||||
{
|
||||
Serializable fieldValue = record.getValue(field.getName());
|
||||
String fieldName = fieldPair.getA() + fieldPair.getB().getName();
|
||||
Serializable fieldValue = record.getValue(fieldName);
|
||||
|
||||
/////////////////////////////////////////
|
||||
// ignore null and empty-string values //
|
||||
/////////////////////////////////////////
|
||||
if(!StringUtils.hasContent(ValueUtils.getValueAsString(fieldValue)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// check if value is already cached //
|
||||
//////////////////////////////////////
|
||||
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
|
||||
possibleValueCache.putIfAbsent(possibleValueSource.getName(), new HashMap<>());
|
||||
Map<Serializable, String> cacheForPvs = possibleValueCache.get(possibleValueSource.getName());
|
||||
QPossibleValueSource possibleValueSource = pvsesByTable.get(tableName).get(0);
|
||||
Map<Serializable, String> cacheForPvs = possibleValueCache.computeIfAbsent(possibleValueSource.getName(), x -> new HashMap<>());
|
||||
|
||||
if(!cacheForPvs.containsKey(fieldValue))
|
||||
{
|
||||
@ -379,6 +457,34 @@ public class QPossibleValueTranslator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Helper for the primePvsCache method
|
||||
*******************************************************************************/
|
||||
private void primePvsCacheTableListingHashLoader(QTableMetaData table, ListingHash<String, Pair<String, QFieldMetaData>> fieldsByPvsTable, ListingHash<String, QPossibleValueSource> pvsesByTable, String fieldNamePrefix, String tableName, Set<String> limitedToFieldNames)
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
|
||||
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
|
||||
{
|
||||
if(limitedToFieldNames != null && !limitedToFieldNames.contains(fieldNamePrefix + field.getName()))
|
||||
{
|
||||
LOG.debug("Skipping cache priming for translation of possible value field [" + fieldNamePrefix + field.getName() + "] - it's not in the limitedToFieldNames set.");
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldsByPvsTable.add(possibleValueSource.getTableName(), Pair.of(fieldNamePrefix, field));
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// todo - optimization we can put the same PVS in this listing hash multiple times... either check for dupes, or change to a set, or something smarter. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
pvsesByTable.add(possibleValueSource.getTableName(), possibleValueSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a given table, and a list of pkey-values in that table, AND a list of
|
||||
** possible value sources based on that table (maybe usually 1, but could be more,
|
||||
@ -398,20 +504,45 @@ public class QPossibleValueTranslator
|
||||
|
||||
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
|
||||
{
|
||||
QueryInput queryInput = new QueryInput(qInstance);
|
||||
queryInput.setSession(session);
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(tableName);
|
||||
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this is needed to get record labels, which are what we use here... unclear if best! //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(notTooDeep())
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// when querying for possible values, we do want to generate their display values, which makes record labels, which are usually used as PVS labels //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// also, if this table uses any possible value fields as part of its own record label, then THOSE possible values need translated. //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
Set<String> possibleValueFieldsToTranslate = new HashSet<>();
|
||||
for(QPossibleValueSource possibleValueSource : possibleValueSources)
|
||||
{
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setShouldGenerateDisplayValues(true);
|
||||
if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
|
||||
{
|
||||
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
|
||||
for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields()))
|
||||
{
|
||||
QFieldMetaData field = table.getField(recordLabelField);
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
possibleValueFieldsToTranslate.add(field.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// an earlier version of this code got into stack overflows, so do a "cheap" check for recursion depth too... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!possibleValueFieldsToTranslate.isEmpty() && notTooDeep())
|
||||
{
|
||||
queryInput.setShouldTranslatePossibleValues(true);
|
||||
queryInput.setFieldsToTranslatePossibleValues(possibleValueFieldsToTranslate);
|
||||
}
|
||||
|
||||
LOG.debug("Priming PVS cache for [" + page.size() + "] ids from [" + tableName + "] table.");
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -26,16 +26,16 @@ import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
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.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -44,7 +44,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class QValueFormatter
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(QValueFormatter.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class);
|
||||
|
||||
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a");
|
||||
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
@ -54,7 +54,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
** For a field, and its value, apply the field's displayFormat.
|
||||
*******************************************************************************/
|
||||
public String formatValue(QFieldMetaData field, Serializable value)
|
||||
public static String formatValue(QFieldMetaData field, Serializable value)
|
||||
{
|
||||
if(QFieldType.BOOLEAN.equals(field.getType()))
|
||||
{
|
||||
@ -81,7 +81,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
** For a display format string (e.g., %d), and a value, apply the displayFormat.
|
||||
*******************************************************************************/
|
||||
public String formatValue(String displayFormat, Serializable value)
|
||||
public static String formatValue(String displayFormat, Serializable value)
|
||||
{
|
||||
return (formatValue(displayFormat, "", value));
|
||||
}
|
||||
@ -92,7 +92,7 @@ public class QValueFormatter
|
||||
** For a display format string, an optional fieldName (only used for logging),
|
||||
** and a value, apply the format.
|
||||
*******************************************************************************/
|
||||
private String formatValue(String displayFormat, String fieldName, Serializable value)
|
||||
private static String formatValue(String displayFormat, String fieldName, Serializable value)
|
||||
{
|
||||
//////////////////////////////////
|
||||
// null values get null results //
|
||||
@ -151,7 +151,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String formatDate(LocalDate date)
|
||||
public static String formatDate(LocalDate date)
|
||||
{
|
||||
return (dateFormatter.format(date));
|
||||
}
|
||||
@ -161,7 +161,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String formatDateTime(LocalDateTime dateTime)
|
||||
public static String formatDateTime(LocalDateTime dateTime)
|
||||
{
|
||||
return (dateTimeFormatter.format(dateTime));
|
||||
}
|
||||
@ -171,7 +171,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
** Make a string from a table's recordLabelFormat and fields, for a given record.
|
||||
*******************************************************************************/
|
||||
public String formatRecordLabel(QTableMetaData table, QRecord record)
|
||||
public static String formatRecordLabel(QTableMetaData table, QRecord record)
|
||||
{
|
||||
if(!StringUtils.hasContent(table.getRecordLabelFormat()))
|
||||
{
|
||||
@ -195,7 +195,7 @@ public class QValueFormatter
|
||||
** For a given format string, and a list of fields, look in displayValueMap and
|
||||
** rawValueMap to get the values to apply to the format.
|
||||
*******************************************************************************/
|
||||
private String formatStringWithFields(String formatString, List<String> formatFields, Map<String, String> displayValueMap, Map<String, Serializable> rawValueMap)
|
||||
private static String formatStringWithFields(String formatString, List<String> formatFields, Map<String, String> displayValueMap, Map<String, Serializable> rawValueMap)
|
||||
{
|
||||
List<Serializable> values = formatFields.stream()
|
||||
.map(fieldName ->
|
||||
@ -221,7 +221,7 @@ public class QValueFormatter
|
||||
** For a given format string, and a list of values, apply the format. Note, null
|
||||
** values in the list become "".
|
||||
*******************************************************************************/
|
||||
public String formatStringWithValues(String formatString, List<String> formatValues)
|
||||
public static String formatStringWithValues(String formatString, List<String> formatValues)
|
||||
{
|
||||
List<String> values = formatValues.stream()
|
||||
.map(v -> v == null ? "" : v)
|
||||
@ -234,7 +234,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
** Deal with non-happy-path cases for making a record label.
|
||||
*******************************************************************************/
|
||||
private String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
|
||||
private static String formatRecordLabelExceptionalCases(QTableMetaData table, QRecord record)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// if there's no record label format, then just return the primary key display value //
|
||||
@ -262,7 +262,7 @@ public class QValueFormatter
|
||||
/*******************************************************************************
|
||||
** For a list of records, set their recordLabels and display values
|
||||
*******************************************************************************/
|
||||
public void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
|
||||
public static void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
|
||||
{
|
||||
if(records == null)
|
||||
{
|
||||
@ -271,17 +271,26 @@ public class QValueFormatter
|
||||
|
||||
for(QRecord record : records)
|
||||
{
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
if(record.getDisplayValue(field.getName()) == null)
|
||||
{
|
||||
String formattedValue = formatValue(field, record.getValue(field.getName()));
|
||||
record.setDisplayValue(field.getName(), formattedValue);
|
||||
}
|
||||
}
|
||||
|
||||
setDisplayValuesInRecord(table.getFields().values(), record);
|
||||
record.setRecordLabel(formatRecordLabel(table, record));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** For a list of records, set their display values
|
||||
*******************************************************************************/
|
||||
public static void setDisplayValuesInRecord(Collection<QFieldMetaData> fields, QRecord record)
|
||||
{
|
||||
for(QFieldMetaData field : fields)
|
||||
{
|
||||
if(record.getDisplayValue(field.getName()) == null)
|
||||
{
|
||||
String formattedValue = formatValue(field, record.getValue(field.getName()));
|
||||
record.setDisplayValue(field.getName(), formattedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
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.query.QCriteriaOperator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||
@ -42,11 +43,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
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.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import org.apache.commons.lang.NotImplementedException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -55,7 +55,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class SearchPossibleValueSourceAction
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(SearchPossibleValueSourceAction.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(SearchPossibleValueSourceAction.class);
|
||||
|
||||
private QPossibleValueTranslator possibleValueTranslator;
|
||||
|
||||
@ -105,13 +105,15 @@ public class SearchPossibleValueSourceAction
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
|
||||
List<Serializable> matchingIds = new ArrayList<>();
|
||||
|
||||
List<?> inputIdsAsCorrectType = convertInputIdsToEnumIdType(possibleValueSource, input.getIdList());
|
||||
|
||||
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
|
||||
{
|
||||
boolean match = false;
|
||||
|
||||
if(input.getIdList() != null)
|
||||
{
|
||||
if(input.getIdList().contains(possibleValue.getId()))
|
||||
if(inputIdsAsCorrectType.contains(possibleValue.getId()))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
@ -146,6 +148,44 @@ public class SearchPossibleValueSourceAction
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The input list of ids might come through as a type that isn't the same as
|
||||
** the type of the ids in the enum (e.g., strings from a frontend, integers
|
||||
** in an enum). So, this method looks at the first id in the enum, and then
|
||||
** maps all the inputIds to be of the same type.
|
||||
*******************************************************************************/
|
||||
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
|
||||
{
|
||||
List<Object> rs = new ArrayList<>();
|
||||
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
|
||||
{
|
||||
return (rs);
|
||||
}
|
||||
|
||||
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
|
||||
|
||||
if(anIdFromTheEnum instanceof Integer)
|
||||
{
|
||||
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsInteger(id)));
|
||||
}
|
||||
else if(anIdFromTheEnum instanceof String)
|
||||
{
|
||||
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsString(id)));
|
||||
}
|
||||
else if(anIdFromTheEnum instanceof Boolean)
|
||||
{
|
||||
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsBoolean(id)));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName());
|
||||
}
|
||||
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -153,15 +193,13 @@ public class SearchPossibleValueSourceAction
|
||||
{
|
||||
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
|
||||
|
||||
QueryInput queryInput = new QueryInput(input.getInstance());
|
||||
queryInput.setSession(input.getSession());
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(possibleValueSource.getTableName());
|
||||
|
||||
QTableMetaData table = input.getInstance().getTable(possibleValueSource.getTableName());
|
||||
|
||||
QQueryFilter queryFilter = new QQueryFilter();
|
||||
queryFilter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(queryFilter);
|
||||
|
||||
if(input.getIdList() != null)
|
||||
{
|
||||
@ -207,11 +245,19 @@ public class SearchPossibleValueSourceAction
|
||||
|
||||
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
|
||||
|
||||
// todo - default filter
|
||||
|
||||
// todo - skip & limit as params
|
||||
queryInput.setLimit(250);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if given a default filter, make it the 'top level' filter and the one we just created a subfilter //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(input.getDefaultQueryFilter() != null)
|
||||
{
|
||||
input.getDefaultQueryFilter().addSubFilter(queryFilter);
|
||||
queryFilter = input.getDefaultQueryFilter();
|
||||
}
|
||||
queryInput.setFilter(queryFilter);
|
||||
|
||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||
List<Serializable> ids = queryOutput.getRecords().stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList();
|
||||
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, ids);
|
||||
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
|
||||
import java.util.Stack;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** record containing the values managed by QContext.
|
||||
*******************************************************************************/
|
||||
public record CapturedContext(QInstance qInstance, QSession qSession, QBackendTransaction qBackendTransaction, Stack<AbstractActionInput> actionStack)
|
||||
{
|
||||
|
||||
}
|
@ -0,0 +1,246 @@
|
||||
/*
|
||||
* 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.context;
|
||||
|
||||
|
||||
import java.util.Stack;
|
||||
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A collection of thread-local variables, to define the current context of the
|
||||
** QQQ code that is running. e.g., what QInstance is being used, what QSession
|
||||
** is active, etc.
|
||||
*******************************************************************************/
|
||||
public class QContext
|
||||
{
|
||||
private static final QLogger LOG = QLogger.getLogger(QContext.class);
|
||||
|
||||
private static ThreadLocal<QInstance> qInstanceThreadLocal = new ThreadLocal<>();
|
||||
private static ThreadLocal<QSession> qSessionThreadLocal = new ThreadLocal<>();
|
||||
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
|
||||
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** private constructor - class is not meant to be instantiated.
|
||||
*******************************************************************************/
|
||||
private QContext()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Most common method to set or init the context - e.g., set the current thread
|
||||
** with a QInstance and QSession.
|
||||
*******************************************************************************/
|
||||
public static void init(QInstance qInstance, QSession qSession)
|
||||
{
|
||||
init(qInstance, qSession, null, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Full flavor init method - also take a transaction and action input (to seed the stack).
|
||||
*******************************************************************************/
|
||||
public static void init(QInstance qInstance, QSession qSession, QBackendTransaction transaction, AbstractActionInput actionInput)
|
||||
{
|
||||
qInstanceThreadLocal.set(qInstance);
|
||||
qSessionThreadLocal.set(qSession);
|
||||
qBackendTransactionThreadLocal.set(transaction);
|
||||
|
||||
actionStackThreadLocal.set(new Stack<>());
|
||||
if(actionInput != null)
|
||||
{
|
||||
actionStackThreadLocal.get().add(actionInput);
|
||||
}
|
||||
|
||||
if(!qInstance.getHasBeenValidated())
|
||||
{
|
||||
try
|
||||
{
|
||||
new QInstanceValidator().validate(qInstance);
|
||||
}
|
||||
catch(QInstanceValidationException e)
|
||||
{
|
||||
LOG.warn(e);
|
||||
throw (new IllegalArgumentException("QInstance failed validation" + e.getMessage(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Init a new thread with the context captured from a different thread. e.g.,
|
||||
** when starting some async task.
|
||||
*******************************************************************************/
|
||||
public static void init(CapturedContext capturedContext)
|
||||
{
|
||||
init(capturedContext.qInstance(), capturedContext.qSession(), capturedContext.qBackendTransaction(), null);
|
||||
actionStackThreadLocal.set(capturedContext.actionStack());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Capture all values from the current thread - meant to be used with the init
|
||||
** overload that takes a CapturedContext, for setting up a child thread.
|
||||
*******************************************************************************/
|
||||
public static CapturedContext capture()
|
||||
{
|
||||
return (new CapturedContext(getQInstance(), getQSession(), getQBackendTransaction(), getActionStack()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Clear all values in the current thread.
|
||||
*******************************************************************************/
|
||||
public static void clear()
|
||||
{
|
||||
qInstanceThreadLocal.remove();
|
||||
qSessionThreadLocal.remove();
|
||||
qBackendTransactionThreadLocal.remove();
|
||||
actionStackThreadLocal.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QInstance getQInstance()
|
||||
{
|
||||
return (qInstanceThreadLocal.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QSession getQSession()
|
||||
{
|
||||
return (qSessionThreadLocal.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QBackendTransaction getQBackendTransaction()
|
||||
{
|
||||
return (qBackendTransactionThreadLocal.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static Stack<AbstractActionInput> getActionStack()
|
||||
{
|
||||
return (actionStackThreadLocal.get());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void pushAction(AbstractActionInput action)
|
||||
{
|
||||
if(actionStackThreadLocal.get() == null)
|
||||
{
|
||||
actionStackThreadLocal.set(new Stack<>());
|
||||
}
|
||||
actionStackThreadLocal.get().push(action);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void popAction()
|
||||
{
|
||||
try
|
||||
{
|
||||
actionStackThreadLocal.get().pop();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error popping action stack", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setQInstance(QInstance qInstance)
|
||||
{
|
||||
qInstanceThreadLocal.set(qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setQSession(QSession qSession)
|
||||
{
|
||||
qSessionThreadLocal.set(qSession);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void setTransaction(QBackendTransaction transaction)
|
||||
{
|
||||
qBackendTransactionThreadLocal.set(transaction);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static void clearTransaction()
|
||||
{
|
||||
qBackendTransactionThreadLocal.remove();
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.exceptions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Exception thrown if user doesn't have permission for an action
|
||||
*
|
||||
*******************************************************************************/
|
||||
public class QPermissionDeniedException extends QException
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QPermissionDeniedException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message & cause
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QPermissionDeniedException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.exceptions;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* Base class for unchecked exceptions thrown in qqq.
|
||||
*
|
||||
*******************************************************************************/
|
||||
public class QRuntimeException extends RuntimeException
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRuntimeException(Throwable t)
|
||||
{
|
||||
super(t.getMessage(), t);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRuntimeException(String message)
|
||||
{
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor of message & cause
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QRuntimeException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -32,8 +32,13 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
@ -42,6 +47,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
|
||||
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.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType;
|
||||
@ -66,8 +73,6 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
|
||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -77,7 +82,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class QInstanceEnricher
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(QInstanceEnricher.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QInstanceEnricher.class);
|
||||
|
||||
private final QInstance qInstance;
|
||||
|
||||
@ -133,6 +138,21 @@ public class QInstanceEnricher
|
||||
{
|
||||
qInstance.getPossibleValueSources().values().forEach(this::enrichPossibleValueSource);
|
||||
}
|
||||
|
||||
if(qInstance.getWidgets() != null)
|
||||
{
|
||||
qInstance.getWidgets().values().forEach(this::enrichWidget);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void enrichWidget(QWidgetMetaDataInterface widgetMetaData)
|
||||
{
|
||||
enrichPermissionRules(widgetMetaData);
|
||||
}
|
||||
|
||||
|
||||
@ -175,6 +195,60 @@ public class QInstanceEnricher
|
||||
{
|
||||
table.setRecordLabelFormat(String.join(" ", Collections.nCopies(table.getRecordLabelFields().size(), "%s")));
|
||||
}
|
||||
|
||||
enrichPermissionRules(table);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void enrichPermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
// make sure there's a permissionsRule object in the metaData object //
|
||||
///////////////////////////////////////////////////////////////////////
|
||||
if(metaDataWithPermissionRules.getPermissionRules() == null)
|
||||
{
|
||||
if(qInstance.getDefaultPermissionRules() != null)
|
||||
{
|
||||
metaDataWithPermissionRules.setPermissionRules(qInstance.getDefaultPermissionRules().clone());
|
||||
}
|
||||
else
|
||||
{
|
||||
metaDataWithPermissionRules.setPermissionRules(QPermissionRules.defaultInstance().clone());
|
||||
}
|
||||
}
|
||||
|
||||
QPermissionRules permissionRules = metaDataWithPermissionRules.getPermissionRules();
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// now make sure the required fields are all set in the permissionRules object //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
if(permissionRules.getLevel() == null)
|
||||
{
|
||||
if(qInstance.getDefaultPermissionRules() != null && qInstance.getDefaultPermissionRules().getLevel() != null)
|
||||
{
|
||||
permissionRules.setLevel(qInstance.getDefaultPermissionRules().getLevel());
|
||||
}
|
||||
else
|
||||
{
|
||||
permissionRules.setLevel(QPermissionRules.defaultInstance().getLevel());
|
||||
}
|
||||
}
|
||||
|
||||
if(permissionRules.getDenyBehavior() == null)
|
||||
{
|
||||
if(qInstance.getDefaultPermissionRules() != null && qInstance.getDefaultPermissionRules().getDenyBehavior() != null)
|
||||
{
|
||||
permissionRules.setDenyBehavior(qInstance.getDefaultPermissionRules().getDenyBehavior());
|
||||
}
|
||||
else
|
||||
{
|
||||
permissionRules.setDenyBehavior(QPermissionRules.defaultInstance().getDenyBehavior());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -193,6 +267,8 @@ public class QInstanceEnricher
|
||||
{
|
||||
process.getStepList().forEach(this::enrichStep);
|
||||
}
|
||||
|
||||
enrichPermissionRules(process);
|
||||
}
|
||||
|
||||
|
||||
@ -323,6 +399,8 @@ public class QInstanceEnricher
|
||||
{
|
||||
enrichAppSection(section);
|
||||
}
|
||||
|
||||
enrichPermissionRules(app);
|
||||
}
|
||||
|
||||
|
||||
@ -411,6 +489,8 @@ public class QInstanceEnricher
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enrichPermissionRules(report);
|
||||
}
|
||||
|
||||
|
||||
@ -506,7 +586,9 @@ public class QInstanceEnricher
|
||||
.withName(processName)
|
||||
.withLabel(table.getLabel() + " Bulk Insert")
|
||||
.withTableName(table.getName())
|
||||
.withIsHidden(true);
|
||||
.withIsHidden(true)
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().clone()
|
||||
.withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class, QCodeUsage.CUSTOMIZER)));
|
||||
|
||||
List<QFieldMetaData> editableFields = new ArrayList<>();
|
||||
for(QFieldSection section : CollectionUtils.nonNullList(table.getSections()))
|
||||
@ -568,7 +650,9 @@ public class QInstanceEnricher
|
||||
.withName(processName)
|
||||
.withLabel(table.getLabel() + " Bulk Edit")
|
||||
.withTableName(table.getName())
|
||||
.withIsHidden(true);
|
||||
.withIsHidden(true)
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().clone()
|
||||
.withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class, QCodeUsage.CUSTOMIZER)));
|
||||
|
||||
List<QFieldMetaData> editableFields = table.getFields().values().stream()
|
||||
.filter(QFieldMetaData::getIsEditable)
|
||||
@ -613,7 +697,9 @@ public class QInstanceEnricher
|
||||
.withName(processName)
|
||||
.withLabel(table.getLabel() + " Bulk Delete")
|
||||
.withTableName(table.getName())
|
||||
.withIsHidden(true);
|
||||
.withIsHidden(true)
|
||||
.withPermissionRules(qInstance.getDefaultPermissionRules().clone()
|
||||
.withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class, QCodeUsage.CUSTOMIZER)));
|
||||
|
||||
List<QFieldMetaData> tableFields = table.getFields().values().stream().toList();
|
||||
process.getFrontendStep("review").setRecordListFields(tableFields);
|
||||
|
@ -39,15 +39,20 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
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.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.code.QCodeReference;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
|
||||
@ -56,7 +61,10 @@ 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.queues.SQSQueueProviderMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
@ -65,10 +73,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -83,7 +91,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class QInstanceValidator
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QInstanceValidator.class);
|
||||
|
||||
private boolean printWarnings = false;
|
||||
|
||||
@ -131,6 +139,8 @@ public class QInstanceValidator
|
||||
validateApps(qInstance);
|
||||
validatePossibleValueSources(qInstance);
|
||||
validateQueuesAndProviders(qInstance);
|
||||
validateJoins(qInstance);
|
||||
validateSecurityKeyTypes(qInstance);
|
||||
|
||||
validateUniqueTopLevelNames(qInstance);
|
||||
}
|
||||
@ -149,6 +159,80 @@ public class QInstanceValidator
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateSecurityKeyTypes(QInstance qInstance)
|
||||
{
|
||||
Set<String> usedNames = new HashSet<>();
|
||||
qInstance.getSecurityKeyTypes().forEach((name, securityKeyType) ->
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyType.getName()), "Missing name for a securityKeyType"))
|
||||
{
|
||||
assertCondition(Objects.equals(name, securityKeyType.getName()), "Inconsistent naming for securityKeyType: " + name + "/" + securityKeyType.getName() + ".");
|
||||
assertCondition(!usedNames.contains(name), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + name);
|
||||
usedNames.add(name);
|
||||
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
|
||||
{
|
||||
assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName());
|
||||
usedNames.add(securityKeyType.getAllAccessKeyName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName()))
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateJoins(QInstance qInstance)
|
||||
{
|
||||
qInstance.getJoins().forEach((joinName, join) ->
|
||||
{
|
||||
assertCondition(Objects.equals(joinName, join.getName()), "Inconsistent naming for join: " + joinName + "/" + join.getName() + ".");
|
||||
|
||||
assertCondition(StringUtils.hasContent(join.getLeftTable()), "Missing left-table name in join: " + joinName);
|
||||
assertCondition(StringUtils.hasContent(join.getRightTable()), "Missing right-table name in join: " + joinName);
|
||||
assertCondition(join.getType() != null, "Missing type for join: " + joinName);
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(join.getJoinOns()), "Missing joinOns for join: " + joinName);
|
||||
|
||||
boolean leftTableExists = assertCondition(qInstance.getTable(join.getLeftTable()) != null, "Left-table name " + join.getLeftTable() + " join " + joinName + " is not a defined table in this instance.");
|
||||
boolean rightTableExists = assertCondition(qInstance.getTable(join.getRightTable()) != null, "Right-table name " + join.getRightTable() + " join " + joinName + " is not a defined table in this instance.");
|
||||
|
||||
for(JoinOn joinOn : CollectionUtils.nonNullList(join.getJoinOns()))
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(joinOn.getLeftField()), "Missing left-field name in a joinOn for join: " + joinName);
|
||||
assertCondition(StringUtils.hasContent(joinOn.getRightField()), "Missing right-field name in a joinOn for join: " + joinName);
|
||||
|
||||
if(leftTableExists)
|
||||
{
|
||||
assertNoException(() -> qInstance.getTable(join.getLeftTable()).getField(joinOn.getLeftField()), "Left field name in joinOn " + joinName + " is not a defined field in table " + join.getLeftTable());
|
||||
}
|
||||
|
||||
if(rightTableExists)
|
||||
{
|
||||
assertNoException(() -> qInstance.getTable(join.getRightTable()).getField(joinOn.getRightField()), "Right field name in joinOn " + joinName + " is not a defined field in table " + join.getRightTable());
|
||||
}
|
||||
}
|
||||
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(join.getOrderBys()))
|
||||
{
|
||||
if(rightTableExists)
|
||||
{
|
||||
assertNoException(() -> qInstance.getTable(join.getRightTable()).getField(orderBy.getFieldName()), "Field name " + orderBy.getFieldName() + " in orderBy for join " + joinName + " is not a defined field in the right-table " + join.getRightTable());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** there can be some unexpected bad-times if you have a table and process, or
|
||||
** table and app (etc) with the same name (e.g., in app tree building). So,
|
||||
@ -272,7 +356,6 @@ public class QInstanceValidator
|
||||
qInstance.getTables().forEach((tableName, table) ->
|
||||
{
|
||||
assertCondition(Objects.equals(tableName, table.getName()), "Inconsistent naming for table: " + tableName + "/" + table.getName() + ".");
|
||||
validateAppChildHasValidParentAppName(qInstance, table);
|
||||
|
||||
////////////////////////////////////////
|
||||
// validate the backend for the table //
|
||||
@ -305,14 +388,7 @@ public class QInstanceValidator
|
||||
{
|
||||
table.getFields().forEach((fieldName, field) ->
|
||||
{
|
||||
assertCondition(Objects.equals(fieldName, field.getName()),
|
||||
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
|
||||
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
|
||||
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
|
||||
}
|
||||
validateTableField(qInstance, tableName, fieldName, field);
|
||||
});
|
||||
}
|
||||
|
||||
@ -342,17 +418,11 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(table.getFields()))
|
||||
for(String fieldName : CollectionUtils.nonNullMap(table.getFields()).keySet())
|
||||
{
|
||||
for(String fieldName : table.getFields().keySet())
|
||||
{
|
||||
assertCondition(fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections.");
|
||||
}
|
||||
assertCondition(fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections.");
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
// validate the record label //
|
||||
///////////////////////////////
|
||||
if(table.getRecordLabelFields() != null && table.getFields() != null)
|
||||
{
|
||||
for(String recordLabelField : table.getRecordLabelFields())
|
||||
@ -361,38 +431,160 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
if(table.getCustomizers() != null)
|
||||
for(Map.Entry<String, QCodeReference> entry : CollectionUtils.nonNullMap(table.getCustomizers()).entrySet())
|
||||
{
|
||||
for(Map.Entry<String, QCodeReference> entry : table.getCustomizers().entrySet())
|
||||
validateTableCustomizer(tableName, entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
validateTableAutomationDetails(qInstance, table);
|
||||
validateTableUniqueKeys(table);
|
||||
validateAssociatedScripts(table);
|
||||
validateTableCacheOf(qInstance, table);
|
||||
validateTableRecordSecurityLocks(qInstance, table);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateTableRecordSecurityLocks(QInstance qInstance, QTableMetaData table)
|
||||
{
|
||||
String prefix = "Table " + table.getName() + " ";
|
||||
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
|
||||
{
|
||||
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
|
||||
}
|
||||
|
||||
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
|
||||
|
||||
boolean hasAnyBadJoins = false;
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
|
||||
{
|
||||
hasAnyBadJoins = true;
|
||||
}
|
||||
}
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName();
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// don't bother trying to validate field names if we know we have a bad join. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
|
||||
{
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
{
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(table.getName()))
|
||||
{
|
||||
validateTableCustomizer(tableName, entry.getKey(), entry.getValue());
|
||||
joins.add(new QueryJoin(join));
|
||||
}
|
||||
else if(join.getRightTable().equals(table.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// validate the table's automations //
|
||||
//////////////////////////////////////
|
||||
if(table.getAutomationDetails() != null)
|
||||
{
|
||||
validateTableAutomationDetails(qInstance, table);
|
||||
}
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// validate the table's unique keys //
|
||||
//////////////////////////////////////
|
||||
if(table.getUniqueKeys() != null)
|
||||
{
|
||||
validateTableUniqueKeys(table);
|
||||
}
|
||||
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// validate the table's associated scripts //
|
||||
/////////////////////////////////////////////
|
||||
if(table.getAssociatedScripts() != null)
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateTableField(QInstance qInstance, String tableName, String fieldName, QFieldMetaData field)
|
||||
{
|
||||
assertCondition(Objects.equals(fieldName, field.getName()),
|
||||
"Inconsistent naming in table " + tableName + " for field " + fieldName + "/" + field.getName() + ".");
|
||||
|
||||
if(field.getPossibleValueSourceName() != null)
|
||||
{
|
||||
assertCondition(qInstance.getPossibleValueSource(field.getPossibleValueSourceName()) != null,
|
||||
"Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + ".");
|
||||
}
|
||||
|
||||
String prefix = "Field " + fieldName + " in table " + tableName + " ";
|
||||
|
||||
ValueTooLongBehavior behavior = field.getBehavior(qInstance, ValueTooLongBehavior.class);
|
||||
if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH))
|
||||
{
|
||||
assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength.");
|
||||
}
|
||||
|
||||
if(field.getMaxLength() != null)
|
||||
{
|
||||
assertCondition(field.getMaxLength() > 0, prefix + "has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0.");
|
||||
assertCondition(field.getType().isStringLike(), prefix + "has maxLength, but is not of a supported type (" + field.getType() + ") - must be a string-like type.");
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// this condition doesn't make sense/apply - because the default value-too-long behavior is pass-through, so, idk, just omit //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// assertCondition(behavior != null, prefix + "specifies a maxLength, but no ValueTooLongBehavior.");
|
||||
}
|
||||
|
||||
FieldSecurityLock fieldSecurityLock = field.getFieldSecurityLock();
|
||||
if(fieldSecurityLock != null)
|
||||
{
|
||||
String securityKeyTypeName = fieldSecurityLock.getSecurityKeyType();
|
||||
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a fieldSecurityLock that is missing a securityKeyType"))
|
||||
{
|
||||
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a fieldSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
|
||||
}
|
||||
|
||||
assertCondition(fieldSecurityLock.getDefaultBehavior() != null, prefix + "has a fieldSecurityLock that is missing a defaultBehavior");
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(fieldSecurityLock.getOverrideValues()), prefix + "has a fieldSecurityLock that is missing overrideValues");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateTableCacheOf(QInstance qInstance, QTableMetaData table)
|
||||
{
|
||||
CacheOf cacheOf = table.getCacheOf();
|
||||
if(cacheOf == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
String prefix = "Table " + table.getName() + " cacheOf ";
|
||||
String sourceTableName = cacheOf.getSourceTable();
|
||||
if(assertCondition(StringUtils.hasContent(sourceTableName), prefix + "is missing a sourceTable name"))
|
||||
{
|
||||
assertCondition(qInstance.getTable(sourceTableName) != null, prefix + "is referencing an unknown sourceTable: " + sourceTableName);
|
||||
|
||||
boolean hasExpirationSeconds = cacheOf.getExpirationSeconds() != null;
|
||||
boolean hasCacheDateFieldName = StringUtils.hasContent(cacheOf.getCachedDateFieldName());
|
||||
assertCondition(hasExpirationSeconds && hasCacheDateFieldName || (!hasExpirationSeconds && !hasCacheDateFieldName), prefix + "is missing either expirationSeconds or cachedDateFieldName (must either have both, or neither.)");
|
||||
|
||||
if(hasCacheDateFieldName)
|
||||
{
|
||||
assertNoException(() -> table.getField(cacheOf.getCachedDateFieldName()), prefix + "cachedDateFieldName " + cacheOf.getCachedDateFieldName() + " is an unrecognized field.");
|
||||
}
|
||||
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(cacheOf.getUseCases()), prefix + "does not have any useCases defined."))
|
||||
{
|
||||
for(CacheUseCase useCase : cacheOf.getUseCases())
|
||||
{
|
||||
validateAssociatedScripts(table);
|
||||
assertCondition(useCase.getType() != null, prefix + "has a useCase without a type.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -404,7 +596,7 @@ public class QInstanceValidator
|
||||
private void validateAssociatedScripts(QTableMetaData table)
|
||||
{
|
||||
Set<String> usedFieldNames = new HashSet<>();
|
||||
for(AssociatedScript associatedScript : table.getAssociatedScripts())
|
||||
for(AssociatedScript associatedScript : CollectionUtils.nonNullList(table.getAssociatedScripts()))
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(associatedScript.getFieldName()), "Table " + table.getName() + " has an associatedScript without a fieldName"))
|
||||
{
|
||||
@ -433,7 +625,7 @@ public class QInstanceValidator
|
||||
private void validateTableUniqueKeys(QTableMetaData table)
|
||||
{
|
||||
Set<Set<String>> ukSets = new HashSet<>();
|
||||
for(UniqueKey uniqueKey : table.getUniqueKeys())
|
||||
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(uniqueKey.getFieldNames()), table.getName() + " has a uniqueKey with no fields"))
|
||||
{
|
||||
@ -458,11 +650,15 @@ public class QInstanceValidator
|
||||
*******************************************************************************/
|
||||
private void validateTableAutomationDetails(QInstance qInstance, QTableMetaData table)
|
||||
{
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
if(automationDetails == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
String tableName = table.getName();
|
||||
String prefix = "Table " + tableName + " automationDetails ";
|
||||
|
||||
QTableAutomationDetails automationDetails = table.getAutomationDetails();
|
||||
|
||||
//////////////////////////////////////
|
||||
// validate the automation provider //
|
||||
//////////////////////////////////////
|
||||
@ -671,6 +867,9 @@ public class QInstanceValidator
|
||||
}
|
||||
else if(!Modifier.isPublic(clazz.getModifiers()))
|
||||
{
|
||||
//////////////////////////////////////////////////////////////
|
||||
// seems like this doesn't get hit, for private classses... //
|
||||
//////////////////////////////////////////////////////////////
|
||||
errors.add(prefix + " because it is not public");
|
||||
}
|
||||
else
|
||||
@ -681,7 +880,7 @@ public class QInstanceValidator
|
||||
boolean hasNoArgConstructor = Stream.of(clazz.getConstructors()).anyMatch(c -> c.getParameterCount() == 0);
|
||||
if(!hasNoArgConstructor)
|
||||
{
|
||||
errors.add(prefix + " because it does not have a parameterless constructor");
|
||||
errors.add(prefix + " because it does not have a public parameterless constructor");
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -787,8 +986,6 @@ public class QInstanceValidator
|
||||
{
|
||||
assertCondition(Objects.equals(processName, process.getName()), "Inconsistent naming for process: " + processName + "/" + process.getName() + ".");
|
||||
|
||||
validateAppChildHasValidParentAppName(qInstance, process);
|
||||
|
||||
/////////////////////////////////////////////
|
||||
// validate the table name for the process //
|
||||
/////////////////////////////////////////////
|
||||
@ -842,7 +1039,6 @@ public class QInstanceValidator
|
||||
qInstance.getReports().forEach((reportName, report) ->
|
||||
{
|
||||
assertCondition(Objects.equals(reportName, report.getName()), "Inconsistent naming for report: " + reportName + "/" + report.getName() + ".");
|
||||
validateAppChildHasValidParentAppName(qInstance, report);
|
||||
|
||||
////////////////////////////////////////
|
||||
// validate dataSources in the report //
|
||||
@ -868,7 +1064,7 @@ public class QInstanceValidator
|
||||
{
|
||||
if(dataSource.getQueryFilter() != null)
|
||||
{
|
||||
validateQueryFilter("In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter());
|
||||
validateQueryFilter(qInstance, "In " + dataSourceErrorPrefix + "query filter - ", qInstance.getTable(dataSource.getSourceTable()), dataSource.getQueryFilter(), dataSource.getQueryJoins());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -883,9 +1079,9 @@ public class QInstanceValidator
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
// validate dataSources in the report //
|
||||
////////////////////////////////////////
|
||||
//////////////////////////////////
|
||||
// validate views in the report //
|
||||
//////////////////////////////////
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(report.getViews()), "At least 1 view must be defined in report " + reportName + "."))
|
||||
{
|
||||
int index = 0;
|
||||
@ -899,19 +1095,30 @@ public class QInstanceValidator
|
||||
usedViewNames.add(view.getName());
|
||||
|
||||
String viewErrorPrefix = "Report " + reportName + " view " + view.getName() + " ";
|
||||
assertCondition(view.getType() != null, viewErrorPrefix + " is missing its type.");
|
||||
if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + " is missing a dataSourceName"))
|
||||
assertCondition(view.getType() != null, viewErrorPrefix + "is missing its type.");
|
||||
if(assertCondition(StringUtils.hasContent(view.getDataSourceName()), viewErrorPrefix + "is missing a dataSourceName"))
|
||||
{
|
||||
assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + " has an unrecognized dataSourceName: " + view.getDataSourceName());
|
||||
assertCondition(usedDataSourceNames.contains(view.getDataSourceName()), viewErrorPrefix + "has an unrecognized dataSourceName: " + view.getDataSourceName());
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(view.getVarianceDataSourceName()))
|
||||
{
|
||||
assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + " has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName());
|
||||
assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + "has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName());
|
||||
}
|
||||
|
||||
// actually, this is okay if there's a customizer, so...
|
||||
assertCondition(CollectionUtils.nullSafeHasContents(view.getColumns()), viewErrorPrefix + " does not have any columns.");
|
||||
boolean hasColumns = CollectionUtils.nullSafeHasContents(view.getColumns());
|
||||
boolean hasViewCustomizer = view.getViewCustomizer() != null;
|
||||
assertCondition(hasColumns || hasViewCustomizer, viewErrorPrefix + "does not have any columns or a view customizer.");
|
||||
|
||||
Set<String> usedColumnNames = new HashSet<>();
|
||||
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
|
||||
{
|
||||
assertCondition(StringUtils.hasContent(column.getName()), viewErrorPrefix + "has a column with no name.");
|
||||
assertCondition(!usedColumnNames.contains(column.getName()), viewErrorPrefix + "has multiple columns named: " + column.getName());
|
||||
usedColumnNames.add(column.getName());
|
||||
|
||||
// todo - is field name valid?
|
||||
}
|
||||
|
||||
// todo - all these too...
|
||||
// view.getPivotFields();
|
||||
@ -931,34 +1138,87 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateQueryFilter(String context, QTableMetaData table, QQueryFilter queryFilter)
|
||||
private void validateQueryFilter(QInstance qInstance, String context, QTableMetaData table, QQueryFilter queryFilter, List<QueryJoin> queryJoins)
|
||||
{
|
||||
for(QFilterCriteria criterion : CollectionUtils.nonNullList(queryFilter.getCriteria()))
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(criterion.getFieldName()), context + "Missing fieldName for a criteria"))
|
||||
String fieldName = criterion.getFieldName();
|
||||
if(assertCondition(StringUtils.hasContent(fieldName), context + "Missing fieldName for a criteria"))
|
||||
{
|
||||
assertNoException(() -> table.getField(criterion.getFieldName()), context + "Criteria fieldName " + criterion.getFieldName() + " is not a field in this table.");
|
||||
assertCondition(findField(qInstance, table, queryJoins, fieldName), context + "Criteria fieldName " + fieldName + " is not a field in this table (or in any given joins).");
|
||||
}
|
||||
assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + criterion.getFieldName());
|
||||
assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + criterion.getFieldName()); // todo - what about ops w/ no value (BLANK)
|
||||
assertCondition(criterion.getOperator() != null, context + "Missing operator for a criteria on fieldName " + fieldName);
|
||||
assertCondition(criterion.getValues() != null, context + "Missing values for a criteria on fieldName " + fieldName); // todo - what about ops w/ no value (BLANK)
|
||||
}
|
||||
|
||||
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(orderBy.getFieldName()), context + "Missing fieldName for an orderBy"))
|
||||
{
|
||||
assertNoException(() -> table.getField(orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table.");
|
||||
assertCondition(findField(qInstance, table, queryJoins, orderBy.getFieldName()), context + "OrderBy fieldName " + orderBy.getFieldName() + " is not a field in this table (or in any given joins).");
|
||||
}
|
||||
}
|
||||
|
||||
for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
|
||||
{
|
||||
validateQueryFilter(context, table, subFilter);
|
||||
validateQueryFilter(qInstance, context, table, subFilter, queryJoins);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Look for a field name in either a table, or the tables referenced in a list of query joins.
|
||||
*******************************************************************************/
|
||||
private boolean findField(QInstance qInstance, QTableMetaData table, List<QueryJoin> queryJoins, String fieldName)
|
||||
{
|
||||
boolean foundField = false;
|
||||
try
|
||||
{
|
||||
table.getField(fieldName);
|
||||
foundField = true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1);
|
||||
|
||||
if(CollectionUtils.nullSafeHasContents(queryJoins))
|
||||
{
|
||||
for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins))
|
||||
{
|
||||
QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable());
|
||||
if(joinTable != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
joinTable.getField(fieldNameAfterDot);
|
||||
foundField = true;
|
||||
break;
|
||||
}
|
||||
catch(Exception e2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add("QInstanceValidator does not yet support finding a field that looks like a join field, but isn't associated with a query.");
|
||||
return (true);
|
||||
// todo! for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
|
||||
// {
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundField;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -980,7 +1240,10 @@ public class QInstanceValidator
|
||||
Set<String> childNames = new HashSet<>();
|
||||
for(QAppChildMetaData child : app.getChildren())
|
||||
{
|
||||
assertCondition(Objects.equals(appName, child.getParentAppName()), "Child " + child.getName() + " of app " + appName + " does not have its parent app properly set.");
|
||||
if(child instanceof QAppMetaData childApp)
|
||||
{
|
||||
assertCondition(Objects.equals(appName, childApp.getParentAppName()), "Child app " + child.getName() + " of app " + appName + " does not have its parent app properly set.");
|
||||
}
|
||||
assertCondition(!childNames.contains(child.getName()), "App " + appName + " contains more than one child named " + child.getName());
|
||||
childNames.add(child.getName());
|
||||
}
|
||||
@ -1206,7 +1469,7 @@ public class QInstanceValidator
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void validateAppChildHasValidParentAppName(QInstance qInstance, QAppChildMetaData appChild)
|
||||
private void validateAppChildHasValidParentAppName(QInstance qInstance, QAppMetaData appChild)
|
||||
{
|
||||
if(appChild.getParentAppName() != null)
|
||||
{
|
||||
|
@ -29,11 +29,10 @@ import java.util.LinkedHashMap;
|
||||
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.ValueUtils;
|
||||
import io.github.cdimascio.dotenv.Dotenv;
|
||||
import io.github.cdimascio.dotenv.DotenvEntry;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -49,7 +48,7 @@ import org.apache.logging.log4j.Logger;
|
||||
*******************************************************************************/
|
||||
public class QMetaDataVariableInterpreter
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class);
|
||||
private static final QLogger LOG = QLogger.getLogger(QMetaDataVariableInterpreter.class);
|
||||
|
||||
private Map<String, String> environmentOverrides;
|
||||
private Map<String, Map<String, Serializable>> valueMaps;
|
||||
|
@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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.instances;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.amazonaws.auth.AWSStaticCredentialsProvider;
|
||||
import com.amazonaws.auth.BasicAWSCredentials;
|
||||
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
|
||||
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder;
|
||||
import com.amazonaws.services.secretsmanager.model.CreateSecretRequest;
|
||||
import com.amazonaws.services.secretsmanager.model.Filter;
|
||||
import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest;
|
||||
import com.amazonaws.services.secretsmanager.model.GetSecretValueResult;
|
||||
import com.amazonaws.services.secretsmanager.model.ListSecretsRequest;
|
||||
import com.amazonaws.services.secretsmanager.model.ListSecretsResult;
|
||||
import com.amazonaws.services.secretsmanager.model.PutSecretValueRequest;
|
||||
import com.amazonaws.services.secretsmanager.model.ResourceExistsException;
|
||||
import com.amazonaws.services.secretsmanager.model.SecretListEntry;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class for working with AWS Secrets Manager.
|
||||
**
|
||||
** Relies on environment variables:
|
||||
** SECRETS_MANAGER_ACCESS_KEY
|
||||
** SECRETS_MANAGER_SECRET_KEY
|
||||
** SECRETS_MANAGER_REGION
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class SecretsManagerUtils
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(SecretsManagerUtils.class);
|
||||
|
||||
private static QMetaDataVariableInterpreter qMetaDataVariableInterpreter;
|
||||
private static AWSSecretsManager _client = null;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** IF secret manager ENV vars are set,
|
||||
** THEN lookup all secrets starting with the given prefix,
|
||||
** and write them to a .env file (backing up any pre-existing .env files first).
|
||||
*******************************************************************************/
|
||||
public static void writeEnvFromSecretsWithNamePrefix(String prefix) throws IOException
|
||||
{
|
||||
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
|
||||
if(optionalSecretsManagerClient.isPresent())
|
||||
{
|
||||
AWSSecretsManager client = optionalSecretsManagerClient.get();
|
||||
|
||||
ListSecretsRequest listSecretsRequest = new ListSecretsRequest().withFilters(new Filter().withKey("name").withValues(prefix));
|
||||
listSecretsRequest.withMaxResults(100);
|
||||
ListSecretsResult listSecretsResult = client.listSecrets(listSecretsRequest);
|
||||
|
||||
StringBuilder fullEnv = new StringBuilder();
|
||||
while(true)
|
||||
{
|
||||
for(SecretListEntry secretListEntry : listSecretsResult.getSecretList())
|
||||
{
|
||||
String nameWithoutPrefix = secretListEntry.getName().replace(prefix, "");
|
||||
Optional<String> secretValue = getSecret(prefix, nameWithoutPrefix);
|
||||
if(secretValue.isPresent())
|
||||
{
|
||||
String envLine = nameWithoutPrefix + "=" + secretValue.get();
|
||||
fullEnv.append(envLine).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if(listSecretsResult.getNextToken() != null)
|
||||
{
|
||||
LOG.trace("Calling for next token...");
|
||||
listSecretsRequest.setNextToken(listSecretsResult.getNextToken());
|
||||
listSecretsResult = client.listSecrets(listSecretsRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
File dotEnv = new File(".env");
|
||||
if(dotEnv.exists())
|
||||
{
|
||||
dotEnv.renameTo(new File(".env.backup-" + System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
FileUtils.writeStringToFile(dotEnv, fullEnv.toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Not writing .env from secrets manager");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Get a single secret value.
|
||||
**
|
||||
** The lookup in secrets manager is done by (path + name). Then, in the value
|
||||
** that comes back, if it looks like JSON, we look for a value inside it under
|
||||
** the key of just "name". Else, if we didn't get JSON back, then we just return
|
||||
** the full text value of the secret.
|
||||
*******************************************************************************/
|
||||
public static Optional<String> getSecret(String path, String name)
|
||||
{
|
||||
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
|
||||
if(optionalSecretsManagerClient.isPresent())
|
||||
{
|
||||
try
|
||||
{
|
||||
AWSSecretsManager client = optionalSecretsManagerClient.get();
|
||||
String secretId = path + name;
|
||||
GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest().withSecretId(secretId);
|
||||
|
||||
GetSecretValueResult getSecretValueResult = client.getSecretValue(getSecretValueRequest);
|
||||
|
||||
try
|
||||
{
|
||||
JSONObject secretJSON = new JSONObject(getSecretValueResult.getSecretString());
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// know we know it's a json object - so - commit to either returning the value under this name, else warning and returning empty //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(secretJSON.has(name))
|
||||
{
|
||||
return (Optional.of(secretJSON.getString(name)));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("SecretsManager secret at [" + secretId + "] was a JSON object, but it did not contain a key of [" + name + "] - so returning empty.");
|
||||
return (Optional.empty());
|
||||
}
|
||||
}
|
||||
catch(JSONException je)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the secret value couldn't be parsed as json, then assume it to be text and just return it //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (Optional.of(getSecretValueResult.getSecretString()));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.debug("Error getting secret from secretManager: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
return (Optional.empty());
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Tries to do a Create - if that fails, then does a Put (update).
|
||||
**
|
||||
** Path is expected to end in a /, but I suppose it isn't strictly required.
|
||||
*******************************************************************************/
|
||||
public static void writeSecret(String path, String name, String value)
|
||||
{
|
||||
JSONObject secretJson = new JSONObject();
|
||||
secretJson.put(name, value);
|
||||
|
||||
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
|
||||
if(optionalSecretsManagerClient.isPresent())
|
||||
{
|
||||
AWSSecretsManager client = optionalSecretsManagerClient.get();
|
||||
|
||||
try
|
||||
{
|
||||
CreateSecretRequest createSecretRequest = new CreateSecretRequest();
|
||||
createSecretRequest.setName(path + name);
|
||||
createSecretRequest.setSecretString(secretJson.toString());
|
||||
client.createSecret(createSecretRequest);
|
||||
}
|
||||
catch(ResourceExistsException e)
|
||||
{
|
||||
PutSecretValueRequest putSecretValueRequest = new PutSecretValueRequest();
|
||||
putSecretValueRequest.setSecretId(path + name);
|
||||
putSecretValueRequest.setSecretString(secretJson.toString());
|
||||
client.putSecretValue(putSecretValueRequest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static Optional<AWSSecretsManager> getSecretsManagerClient()
|
||||
{
|
||||
if(_client == null)
|
||||
{
|
||||
QMetaDataVariableInterpreter interpreter = getQMetaDataVariableInterpreter();
|
||||
|
||||
String accessKey = interpreter.interpret("${env.SECRETS_MANAGER_ACCESS_KEY}");
|
||||
String secretKey = interpreter.interpret("${env.SECRETS_MANAGER_SECRET_KEY}");
|
||||
String region = interpreter.interpret("${env.SECRETS_MANAGER_REGION}");
|
||||
|
||||
if(StringUtils.hasContent(accessKey) && StringUtils.hasContent(secretKey) && StringUtils.hasContent(region))
|
||||
{
|
||||
try
|
||||
{
|
||||
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
|
||||
_client = AWSSecretsManagerClientBuilder.standard()
|
||||
.withCredentials(new AWSStaticCredentialsProvider(credentials))
|
||||
.withRegion(region)
|
||||
.build();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error opening Secrets Manager client", e);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("One or more SECRETS_MANAGER env var was not set. Unable to open Secrets Manager client.");
|
||||
}
|
||||
}
|
||||
|
||||
return (Optional.ofNullable(_client));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QMetaDataVariableInterpreter getQMetaDataVariableInterpreter()
|
||||
{
|
||||
return Objects.requireNonNullElseGet(qMetaDataVariableInterpreter, QMetaDataVariableInterpreter::new);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Ideally meant for tests or one-offs to set up a variable interpreter with
|
||||
** an override ENV.
|
||||
*******************************************************************************/
|
||||
static void setQMetaDataVariableInterpreter(QMetaDataVariableInterpreter qMetaDataVariableInterpreter)
|
||||
{
|
||||
SecretsManagerUtils.qMetaDataVariableInterpreter = qMetaDataVariableInterpreter;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class LogPair
|
||||
{
|
||||
private String key;
|
||||
private Object value;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public LogPair(String key, Object value)
|
||||
{
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
String valueString = getValueString(value);
|
||||
|
||||
return "\"" + Objects.requireNonNullElse(key, "null").replace('"', '.') + "\":" + valueString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String getValueString(Object value)
|
||||
{
|
||||
String valueString;
|
||||
if(value == null)
|
||||
{
|
||||
valueString = "null";
|
||||
}
|
||||
else if(value instanceof LogPair subLogPair)
|
||||
{
|
||||
valueString = '{' + subLogPair.toString() + '}';
|
||||
}
|
||||
else if(value instanceof LogPair[] subLogPairs)
|
||||
{
|
||||
String subLogPairsString = Arrays.stream(subLogPairs).map(LogPair::toString).collect(Collectors.joining(","));
|
||||
valueString = '{' + subLogPairsString + '}';
|
||||
}
|
||||
else if(value instanceof UnsafeSupplier<?, ?> us)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object o = us.get();
|
||||
return getValueString(o);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
valueString = "LogValueError";
|
||||
}
|
||||
}
|
||||
else if(value instanceof Number n)
|
||||
{
|
||||
valueString = String.valueOf(n);
|
||||
}
|
||||
else
|
||||
{
|
||||
valueString = '"' + String.valueOf(value).replace("\"", "\\\"") + '"';
|
||||
}
|
||||
return valueString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for key
|
||||
*******************************************************************************/
|
||||
public String getKey()
|
||||
{
|
||||
return (this.key);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for key
|
||||
*******************************************************************************/
|
||||
public void setKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for key
|
||||
*******************************************************************************/
|
||||
public LogPair withKey(String key)
|
||||
{
|
||||
this.key = key;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for value
|
||||
*******************************************************************************/
|
||||
public Object getValue()
|
||||
{
|
||||
return (this.value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for value
|
||||
*******************************************************************************/
|
||||
public void setValue(Object value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for value
|
||||
*******************************************************************************/
|
||||
public LogPair withValue(Object value)
|
||||
{
|
||||
this.value = value;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class LogUtils
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String jsonLog(List<LogPair> logPairs)
|
||||
{
|
||||
List<LogPair> filteredList = logPairs.stream().filter(Objects::nonNull).toList();
|
||||
if(filteredList.isEmpty())
|
||||
{
|
||||
if(QLogger.processTagLogPairJson != null)
|
||||
{
|
||||
return ("{" + QLogger.processTagLogPairJson + "}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return ("{}");
|
||||
}
|
||||
}
|
||||
|
||||
return ('{' + filteredList.stream().map(LogPair::toString).collect(Collectors.joining(","))
|
||||
+ (QLogger.processTagLogPairJson != null ? (',' + QLogger.processTagLogPairJson) : "") + '}');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static String jsonLog(LogPair... logPairs)
|
||||
{
|
||||
if(logPairs == null || logPairs.length == 0)
|
||||
{
|
||||
return ("{}");
|
||||
}
|
||||
|
||||
return (jsonLog(Arrays.asList(logPairs)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static LogPair logPair(String key, Object value)
|
||||
{
|
||||
return (new LogPair(key, value));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static LogPair logPair(String key, UnsafeSupplier<Object, Exception> valueSupplier)
|
||||
{
|
||||
try
|
||||
{
|
||||
return (new LogPair(key, valueSupplier.get()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return (new LogPair(key, "exceptionLoggingValue: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static LogPair logPair(String key, LogPair... values)
|
||||
{
|
||||
return (new LogPair(key, values));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
static String filterStackTrace(String stackTrace)
|
||||
{
|
||||
try
|
||||
{
|
||||
String packagesToKeep = "com.kingsrook|com.nutrifresh"; // todo - parameterize!!
|
||||
StringBuilder rs = new StringBuilder();
|
||||
String[] lines = stackTrace.split("\n");
|
||||
|
||||
int indexWithinSubStack = 0;
|
||||
int skipsInThisPackage = 0;
|
||||
String packageBeingSkipped = null;
|
||||
|
||||
for(String line : lines)
|
||||
{
|
||||
boolean keepLine = true;
|
||||
|
||||
if(line.matches("^\\s+at .*"))
|
||||
{
|
||||
keepLine = false;
|
||||
indexWithinSubStack++;
|
||||
if(line.matches("^\\s+at (" + packagesToKeep + ").*"))
|
||||
{
|
||||
keepLine = true;
|
||||
}
|
||||
if(indexWithinSubStack == 1)
|
||||
{
|
||||
keepLine = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
indexWithinSubStack = 0;
|
||||
|
||||
if(skipsInThisPackage > 0)
|
||||
{
|
||||
rs.append("\t... ").append(skipsInThisPackage).append(" in ").append(packageBeingSkipped).append("\n");
|
||||
skipsInThisPackage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if(keepLine)
|
||||
{
|
||||
rs.append(line).append("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
String thisPackage = line.replaceFirst("\\s+at ", "").replaceFirst("(\\w+\\.\\w+).*", "$1");
|
||||
if(Objects.equals(thisPackage, packageBeingSkipped))
|
||||
{
|
||||
skipsInThisPackage++;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(skipsInThisPackage > 0)
|
||||
{
|
||||
rs.append("\t... ").append(skipsInThisPackage).append(" in ").append(packageBeingSkipped).append("\n");
|
||||
}
|
||||
skipsInThisPackage = 1;
|
||||
}
|
||||
packageBeingSkipped = thisPackage;
|
||||
}
|
||||
}
|
||||
|
||||
if(rs.length() > 0)
|
||||
{
|
||||
rs.deleteCharAt(rs.length() - 1);
|
||||
}
|
||||
|
||||
return (rs.toString());
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// upon any exception, just return the input //
|
||||
///////////////////////////////////////////////
|
||||
return (stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,525 @@
|
||||
/*
|
||||
* 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.logging;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
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.instances.QMetaDataVariableInterpreter;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
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;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Wrapper for
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class QLogger
|
||||
{
|
||||
private static Map<String, QLogger> loggerMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private static boolean logSessionIdEnabled = true;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// note - read in LogUtils, where log pairs are made into a string. //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
static String processTagLogPairJson = null;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
static
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// read the property to see if sessionIds in log messages is enabled, just once, statically //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String propertyName = "qqq.logger.logSessionId.disabled";
|
||||
String propertyValue = System.getProperty(propertyName, "");
|
||||
if(propertyValue.equals("true"))
|
||||
{
|
||||
logSessionIdEnabled = false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// read the property (or env var) to see if there's a "processTag" to put on all messages //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
try
|
||||
{
|
||||
String processTag = System.getProperty("qqq.logger.processTag");
|
||||
if(processTag == null)
|
||||
{
|
||||
processTag = new QMetaDataVariableInterpreter().interpret("${env.QQQ_LOGGER_PROCESS_TAG}");
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(processTag))
|
||||
{
|
||||
processTagLogPairJson = "\"processTag\":\"" + processTag + "\"";
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QLogger(Logger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public static QLogger getLogger(Class<?> c)
|
||||
{
|
||||
return (loggerMap.computeIfAbsent(c.getName(), x -> new QLogger(LogManager.getLogger(c))));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void log(Level level, String message)
|
||||
{
|
||||
logger.log(level, makeJsonString(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void log(Level level, String message, Throwable t)
|
||||
{
|
||||
logger.log(level, makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void log(Level level, Throwable t)
|
||||
{
|
||||
logger.log(level, makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void trace(String message)
|
||||
{
|
||||
logger.trace(makeJsonString(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void trace(String message, LogPair... logPairs)
|
||||
{
|
||||
logger.trace(makeJsonString(message, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void trace(String message, Object... values)
|
||||
{
|
||||
logger.trace(makeJsonString(message), values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void trace(String message, Throwable t)
|
||||
{
|
||||
logger.trace(makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void trace(Throwable t)
|
||||
{
|
||||
logger.trace(makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void debug(String message)
|
||||
{
|
||||
logger.debug(makeJsonString(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void debug(String message, LogPair... logPairs)
|
||||
{
|
||||
logger.debug(makeJsonString(message, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void debug(String message, Object... values)
|
||||
{
|
||||
logger.debug(makeJsonString(message), values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void debug(String message, Throwable t)
|
||||
{
|
||||
logger.debug(makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void debug(Throwable t)
|
||||
{
|
||||
logger.debug(makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(String message)
|
||||
{
|
||||
logger.info(makeJsonString(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(LogPair... logPairs)
|
||||
{
|
||||
logger.info(makeJsonString(null, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(List<LogPair> logPairList)
|
||||
{
|
||||
logger.info(makeJsonString(null, null, logPairList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(String message, LogPair... logPairs)
|
||||
{
|
||||
logger.info(makeJsonString(message, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(String message, Object... values)
|
||||
{
|
||||
logger.info(makeJsonString(message), values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(String message, Throwable t)
|
||||
{
|
||||
logger.info(makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void info(Throwable t)
|
||||
{
|
||||
logger.info(makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void warn(String message)
|
||||
{
|
||||
logger.warn(makeJsonString(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void warn(String message, LogPair... logPairs)
|
||||
{
|
||||
logger.warn(makeJsonString(message, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void warn(String message, Object... values)
|
||||
{
|
||||
logger.warn(makeJsonString(message), values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void warn(String message, Throwable t)
|
||||
{
|
||||
logger.warn(makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void warn(Throwable t)
|
||||
{
|
||||
logger.warn(makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void error(String message)
|
||||
{
|
||||
logger.error(makeJsonString(message));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void error(String message, LogPair... logPairs)
|
||||
{
|
||||
logger.error(makeJsonString(message, null, logPairs));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void error(String message, Object... values)
|
||||
{
|
||||
logger.error(makeJsonString(message), values);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void error(String message, Throwable t)
|
||||
{
|
||||
logger.error(makeJsonString(message, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void error(Throwable t)
|
||||
{
|
||||
logger.error(makeJsonString(null, t));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String makeJsonString(String message)
|
||||
{
|
||||
return (makeJsonString(message, null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String makeJsonString(String message, Throwable t)
|
||||
{
|
||||
return (makeJsonString(message, t, (List<LogPair>) null));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String makeJsonString(String message, Throwable t, LogPair[] logPairs)
|
||||
{
|
||||
List<LogPair> logPairList = new ArrayList<>();
|
||||
if(logPairs != null)
|
||||
{
|
||||
logPairList.addAll(Arrays.stream(logPairs).toList());
|
||||
}
|
||||
|
||||
return (makeJsonString(message, t, logPairList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private String makeJsonString(String message, Throwable t, List<LogPair> logPairList)
|
||||
{
|
||||
if(logPairList == null)
|
||||
{
|
||||
logPairList = new ArrayList<>();
|
||||
}
|
||||
|
||||
if(StringUtils.hasContent(message))
|
||||
{
|
||||
logPairList.add(0, logPair("message", message));
|
||||
}
|
||||
|
||||
addSessionLogPair(logPairList);
|
||||
|
||||
if(t != null)
|
||||
{
|
||||
logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t))));
|
||||
}
|
||||
|
||||
return (LogUtils.jsonLog(logPairList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static void addSessionLogPair(List<LogPair> logPairList)
|
||||
{
|
||||
if(logSessionIdEnabled)
|
||||
{
|
||||
QSession session = QContext.getQSession();
|
||||
LogPair sessionLogPair;
|
||||
|
||||
if(session == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// note - being careful here to make the same json structure whether session is known or unknown //
|
||||
// (e.g., not a string in one case and an object in another case) - to help loggly. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
sessionLogPair = logPair("session", logPair("id", "unknown"));
|
||||
}
|
||||
else
|
||||
{
|
||||
String user = "unknown";
|
||||
if(session.getUser() != null)
|
||||
{
|
||||
user = session.getUser().getIdReference();
|
||||
}
|
||||
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
logPairList.add(sessionLogPair);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//////////////////////////////////////
|
||||
// deal with not-modifiable list... //
|
||||
//////////////////////////////////////
|
||||
logPairList = new ArrayList<>(logPairList);
|
||||
logPairList.add(sessionLogPair);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,25 +25,22 @@ package com.kingsrook.qqq.backend.core.model.actions;
|
||||
import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
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.authentication.QAuthenticationMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Base input class for all Q actions.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractActionInput
|
||||
public class AbstractActionInput
|
||||
{
|
||||
private static final Logger LOG = LogManager.getLogger(AbstractActionInput.class);
|
||||
|
||||
protected QInstance instance;
|
||||
protected QSession session;
|
||||
private static final QLogger LOG = QLogger.getLogger(AbstractActionInput.class);
|
||||
|
||||
private AsyncJobCallback asyncJobCallback;
|
||||
|
||||
@ -58,19 +55,9 @@ public abstract class AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AbstractActionInput(QInstance instance)
|
||||
{
|
||||
this.instance = instance;
|
||||
validateInstance(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** performance instance validation (if not previously done).
|
||||
* // todo - verify this is happening (e.g., when context is set i guess)
|
||||
*******************************************************************************/
|
||||
private void validateInstance(QInstance instance)
|
||||
{
|
||||
@ -99,7 +86,7 @@ public abstract class AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public QAuthenticationMetaData getAuthenticationMetaData()
|
||||
{
|
||||
return (instance.getAuthentication());
|
||||
return (getInstance().getAuthentication());
|
||||
}
|
||||
|
||||
|
||||
@ -110,19 +97,7 @@ public abstract class AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public QInstance getInstance()
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for instance
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setInstance(QInstance instance)
|
||||
{
|
||||
validateInstance(instance);
|
||||
this.instance = instance;
|
||||
return (QContext.getQInstance());
|
||||
}
|
||||
|
||||
|
||||
@ -133,18 +108,7 @@ public abstract class AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public QSession getSession()
|
||||
{
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for session
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setSession(QSession session)
|
||||
{
|
||||
this.session = session;
|
||||
return (QContext.getQSession());
|
||||
}
|
||||
|
||||
|
||||
@ -175,4 +139,15 @@ public abstract class AbstractActionInput
|
||||
{
|
||||
this.asyncJobCallback = asyncJobCallback;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for instance
|
||||
*******************************************************************************/
|
||||
public AbstractActionInput withInstance(QInstance instance)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,8 +22,8 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
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.QTableMetaData;
|
||||
|
||||
|
||||
@ -31,32 +31,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
** Base class for input for any qqq action that works against a table.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public abstract class AbstractTableActionInput extends AbstractActionInput
|
||||
public class AbstractTableActionInput extends AbstractActionInput
|
||||
{
|
||||
private String tableName;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QBackendMetaData getBackend()
|
||||
{
|
||||
return (instance.getBackendForTable(getTableName()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QTableMetaData getTable()
|
||||
{
|
||||
return (instance.getTable(getTableName()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@ -69,9 +49,19 @@ public abstract class AbstractTableActionInput extends AbstractActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public AbstractTableActionInput(QInstance instance)
|
||||
public QBackendMetaData getBackend()
|
||||
{
|
||||
super(instance);
|
||||
return (QContext.getQInstance().getBackendForTable(getTableName()));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QTableMetaData getTable()
|
||||
{
|
||||
return (QContext.getQInstance().getTable(getTableName()));
|
||||
}
|
||||
|
||||
|
||||
@ -95,4 +85,16 @@ public abstract class AbstractTableActionInput extends AbstractActionInput
|
||||
{
|
||||
this.tableName = tableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for tableName
|
||||
*******************************************************************************/
|
||||
public AbstractTableActionInput withTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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.audits;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class AuditInput extends AbstractActionInput
|
||||
{
|
||||
private String auditTableName;
|
||||
private String auditUserName;
|
||||
private Instant timestamp;
|
||||
private String message;
|
||||
private List<Integer> recordIdList;
|
||||
|
||||
private Map<String, Serializable> securityKeyValues;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for auditTableName
|
||||
*******************************************************************************/
|
||||
public String getAuditTableName()
|
||||
{
|
||||
return (this.auditTableName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for auditTableName
|
||||
*******************************************************************************/
|
||||
public void setAuditTableName(String auditTableName)
|
||||
{
|
||||
this.auditTableName = auditTableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for auditTableName
|
||||
*******************************************************************************/
|
||||
public AuditInput withAuditTableName(String auditTableName)
|
||||
{
|
||||
this.auditTableName = auditTableName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for auditUserName
|
||||
*******************************************************************************/
|
||||
public String getAuditUserName()
|
||||
{
|
||||
return (this.auditUserName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for auditUserName
|
||||
*******************************************************************************/
|
||||
public void setAuditUserName(String auditUserName)
|
||||
{
|
||||
this.auditUserName = auditUserName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for auditUserName
|
||||
*******************************************************************************/
|
||||
public AuditInput withAuditUserName(String auditUserName)
|
||||
{
|
||||
this.auditUserName = auditUserName;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for timestamp
|
||||
*******************************************************************************/
|
||||
public Instant getTimestamp()
|
||||
{
|
||||
return (this.timestamp);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for timestamp
|
||||
*******************************************************************************/
|
||||
public void setTimestamp(Instant timestamp)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for timestamp
|
||||
*******************************************************************************/
|
||||
public AuditInput withTimestamp(Instant timestamp)
|
||||
{
|
||||
this.timestamp = timestamp;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for message
|
||||
*******************************************************************************/
|
||||
public String getMessage()
|
||||
{
|
||||
return (this.message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for message
|
||||
*******************************************************************************/
|
||||
public void setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for message
|
||||
*******************************************************************************/
|
||||
public AuditInput withMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for recordIdList
|
||||
*******************************************************************************/
|
||||
public List<Integer> getRecordIdList()
|
||||
{
|
||||
return (this.recordIdList);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for recordIdList
|
||||
*******************************************************************************/
|
||||
public void setRecordIdList(List<Integer> recordIdList)
|
||||
{
|
||||
this.recordIdList = recordIdList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for recordIdList
|
||||
*******************************************************************************/
|
||||
public AuditInput withRecordIdList(List<Integer> recordIdList)
|
||||
{
|
||||
this.recordIdList = recordIdList;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for securityKeyValues
|
||||
*******************************************************************************/
|
||||
public Map<String, Serializable> getSecurityKeyValues()
|
||||
{
|
||||
return (this.securityKeyValues);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for securityKeyValues
|
||||
*******************************************************************************/
|
||||
public void setSecurityKeyValues(Map<String, Serializable> securityKeyValues)
|
||||
{
|
||||
this.securityKeyValues = securityKeyValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for securityKeyValues
|
||||
*******************************************************************************/
|
||||
public AuditInput withSecurityKeyValues(Map<String, Serializable> securityKeyValues)
|
||||
{
|
||||
this.securityKeyValues = securityKeyValues;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.audits;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class AuditOutput extends AbstractActionOutput
|
||||
{
|
||||
}
|
@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -40,14 +39,4 @@ public class MetaDataInput extends AbstractActionInput
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public MetaDataInput(QInstance instance)
|
||||
{
|
||||
super(instance);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ public class MetaDataOutput extends AbstractActionOutput
|
||||
private Map<String, QFrontendReportMetaData> reports;
|
||||
private Map<String, QFrontendAppMetaData> apps;
|
||||
private Map<String, QFrontendWidgetMetaData> widgets;
|
||||
private Map<String, String> environmentValues;
|
||||
|
||||
private List<AppTreeNode> appTree;
|
||||
private QBrandingMetaData branding;
|
||||
@ -202,4 +203,27 @@ public class MetaDataOutput extends AbstractActionOutput
|
||||
{
|
||||
this.branding = branding;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for environmentValues
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Map<String, String> getEnvironmentValues()
|
||||
{
|
||||
return environmentValues;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for environmentValues
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setEnvironmentValues(Map<String, String> environmentValues)
|
||||
{
|
||||
this.environmentValues = environmentValues;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -45,16 +44,6 @@ public class ProcessMetaDataInput extends AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ProcessMetaDataInput(QInstance instance)
|
||||
{
|
||||
super(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for processName
|
||||
**
|
||||
|
@ -22,19 +22,15 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.metadata;
|
||||
|
||||
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Input for meta-data for a table.
|
||||
**
|
||||
*******************************************************************************/
|
||||
public class TableMetaDataInput extends AbstractActionInput
|
||||
public class TableMetaDataInput extends AbstractTableActionInput
|
||||
{
|
||||
private String tableName;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -43,35 +39,4 @@ public class TableMetaDataInput extends AbstractActionInput
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TableMetaDataInput(QInstance instance)
|
||||
{
|
||||
super(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for tableName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public String getTableName()
|
||||
{
|
||||
return tableName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for tableName
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setTableName(String tableName)
|
||||
{
|
||||
this.tableName = tableName;
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
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.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -422,6 +424,17 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public LogPair toLogPair()
|
||||
{
|
||||
return (logPair("ProcessSummary", logPair("status", status), logPair("count", count), logPair("message", message)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for messageSuffix
|
||||
**
|
||||
|
@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -47,4 +48,8 @@ public interface ProcessSummaryLineInterface extends Serializable
|
||||
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
LogPair toLogPair();
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -50,6 +52,18 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Override
|
||||
public LogPair toLogPair()
|
||||
{
|
||||
return (logPair("ProcessSummary", logPair("status", status), logPair("tableName", tableName), logPair("recordId", recordId),
|
||||
logPair("linkPreText", linkPreText), logPair("linkText", linkText), logPair("linkPostText", linkPostText)));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -31,9 +31,9 @@ import java.util.UUID;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
|
||||
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.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
@ -73,20 +73,8 @@ public class RunBackendStepInput extends AbstractActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunBackendStepInput(QInstance instance)
|
||||
public RunBackendStepInput(ProcessState processState)
|
||||
{
|
||||
super(instance);
|
||||
processState = new ProcessState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunBackendStepInput(QInstance instance, ProcessState processState)
|
||||
{
|
||||
super(instance);
|
||||
this.processState = processState;
|
||||
}
|
||||
|
||||
@ -102,7 +90,6 @@ public class RunBackendStepInput extends AbstractActionInput
|
||||
public void cloneFieldsInto(RunBackendStepInput target)
|
||||
{
|
||||
target.setStepName(getStepName());
|
||||
target.setSession(getSession());
|
||||
target.setTableName(getTableName());
|
||||
target.setProcessName(getProcessName());
|
||||
target.setAsyncJobCallback(getAsyncJobCallback());
|
||||
@ -117,7 +104,7 @@ public class RunBackendStepInput extends AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public QStepMetaData getStepMetaData()
|
||||
{
|
||||
return (instance.getProcessStep(getProcessName(), getStepName()));
|
||||
return (QContext.getQInstance().getProcessStep(getProcessName(), getStepName()));
|
||||
}
|
||||
|
||||
|
||||
@ -200,7 +187,7 @@ public class RunBackendStepInput extends AbstractActionInput
|
||||
return (null);
|
||||
}
|
||||
|
||||
return (instance.getTable(tableName));
|
||||
return (QContext.getQInstance().getTable(tableName));
|
||||
}
|
||||
|
||||
|
||||
|
@ -27,9 +27,9 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
|
||||
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.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
||||
|
||||
|
||||
@ -71,17 +71,6 @@ public class RunProcessInput extends AbstractActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunProcessInput(QInstance instance)
|
||||
{
|
||||
super(instance);
|
||||
processState = new ProcessState();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** e.g., for steps after the first step in a process, seed the data in a run
|
||||
** function request from a process state.
|
||||
@ -99,7 +88,7 @@ public class RunProcessInput extends AbstractActionInput
|
||||
*******************************************************************************/
|
||||
public QProcessMetaData getProcessMetaData()
|
||||
{
|
||||
return (instance.getProcess(getProcessName()));
|
||||
return (QContext.getQInstance().getProcess(getProcessName()));
|
||||
}
|
||||
|
||||
|
||||
|
@ -26,8 +26,6 @@ import java.io.OutputStream;
|
||||
import java.util.List;
|
||||
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.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -56,27 +54,6 @@ public class ExportInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ExportInput(QInstance instance)
|
||||
{
|
||||
super(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ExportInput(QInstance instance, QSession session)
|
||||
{
|
||||
super(instance);
|
||||
setSession(session);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for queryFilter
|
||||
**
|
||||
|
@ -27,6 +27,7 @@ import java.util.function.Supplier;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.CsvExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExcelExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ExportStreamerInterface;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.JsonExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.actions.reporting.ListOfMapsExportStreamer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
@ -39,6 +40,7 @@ import org.dhatim.fastexcel.Worksheet;
|
||||
public enum ReportFormat
|
||||
{
|
||||
XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
JSON(null, null, JsonExportStreamer::new, "application/json"),
|
||||
CSV(null, null, CsvExportStreamer::new, "text/csv"),
|
||||
LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null);
|
||||
|
||||
|
@ -24,10 +24,9 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -53,27 +52,6 @@ public class ReportInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReportInput(QInstance instance)
|
||||
{
|
||||
super(instance);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ReportInput(QInstance instance, QSession session)
|
||||
{
|
||||
super(instance);
|
||||
setSession(session);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for reportName
|
||||
**
|
||||
@ -118,6 +96,20 @@ public class ReportInput extends AbstractTableActionInput
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void addInputValue(String key, Serializable value)
|
||||
{
|
||||
if(this.inputValues == null)
|
||||
{
|
||||
this.inputValues = new HashMap<>();
|
||||
}
|
||||
this.inputValues.put(key, value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for filename
|
||||
**
|
||||
|
@ -27,7 +27,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
|
||||
|
||||
@ -47,9 +46,8 @@ public class ExecuteCodeInput extends AbstractActionInput
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ExecuteCodeInput(QInstance qInstance)
|
||||
public ExecuteCodeInput()
|
||||
{
|
||||
super(qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
@ -24,8 +24,8 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
|
||||
|
||||
|
||||
@ -36,17 +36,19 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
|
||||
{
|
||||
private AssociatedScriptCodeReference codeReference;
|
||||
private Map<String, Serializable> inputValues;
|
||||
private QCodeExecutionLoggerInterface logger;
|
||||
|
||||
private Serializable outputObject;
|
||||
|
||||
private Serializable scriptUtils;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public RunAssociatedScriptInput(QInstance qInstance)
|
||||
public RunAssociatedScriptInput()
|
||||
{
|
||||
super(qInstance);
|
||||
}
|
||||
|
||||
|
||||
@ -151,4 +153,56 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for logger
|
||||
*******************************************************************************/
|
||||
public QCodeExecutionLoggerInterface getLogger()
|
||||
{
|
||||
return (this.logger);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for logger
|
||||
*******************************************************************************/
|
||||
public void setLogger(QCodeExecutionLoggerInterface logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for logger
|
||||
*******************************************************************************/
|
||||
public RunAssociatedScriptInput withLogger(QCodeExecutionLoggerInterface logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for scriptUtils
|
||||
**
|
||||
*******************************************************************************/
|
||||
public Serializable getScriptUtils()
|
||||
{
|
||||
return scriptUtils;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for scriptUtils
|
||||
**
|
||||
*******************************************************************************/
|
||||
public void setScriptUtils(Serializable scriptUtils)
|
||||
{
|
||||
this.scriptUtils = scriptUtils;
|
||||
}
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
|
||||
|
||||
import java.io.Serializable;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -44,9 +43,8 @@ public class StoreAssociatedScriptInput extends AbstractTableActionInput
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public StoreAssociatedScriptInput(QInstance instance)
|
||||
public StoreAssociatedScriptInput()
|
||||
{
|
||||
super(instance);
|
||||
}
|
||||
|
||||
|
||||
|
@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
|
||||
|
||||
@ -42,9 +41,8 @@ public class TestScriptInput extends AbstractTableActionInput
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public TestScriptInput(QInstance qInstance)
|
||||
public TestScriptInput()
|
||||
{
|
||||
super(qInstance);
|
||||
}
|
||||
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user