Compare commits

..

110 Commits

Author SHA1 Message Date
a7faf52817 Merge branch 'release/0.11.0' 2023-01-30 13:37:55 -06:00
5a9a0754a6 Update versions for release 2023-01-30 13:31:17 -06:00
4790f55243 Fixing scheduled process context; better thread names; add serverInfo endpoint 2023-01-26 21:55:24 -06:00
f0450ef621 PRDONE-170 - Adding support for passing in custom ScriptUtls for scripts. 2023-01-26 12:45:07 -06:00
ed5839aa0a Add /run endpoint for running processes w/o any frontend 2023-01-26 11:23:41 -06:00
56f05c74fc Add secrets manager; update org.json 2023-01-26 11:15:47 -06:00
4f8f8bad9a Initial checkin 2023-01-26 11:15:02 -06:00
4a1853faa5 SPRINT-19: added more chart data and widget meta data 2023-01-25 19:13:35 -06:00
c972f9cecc Update to allow _qStepTimeoutMillis to come from formBody 2023-01-25 10:09:45 -06:00
efa796bb39 Add property/env to add processTag to all logs 2023-01-24 15:56:50 -06:00
a05e74a7d0 SPRINT-19: added stacked bar chart, more widget metadata 2023-01-24 15:52:15 -06:00
41fcb09c70 Update to reset memory record store before and after each 2023-01-24 14:22:09 -06:00
6b12390f6c make scheduler look at env var in addition to system property for deciding whether to start or not. 2023-01-24 14:19:55 -06:00
9889320fa6 add call to validate api responses to doInsert (POST) 2023-01-24 14:08:45 -06:00
c00120a1fc add overloads that take 'String message, LogPair... logPairs' 2023-01-24 13:52:44 -06:00
029f436071 Update to cache script and revision ids (could be better); new AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger, to just do 2 inserts across multiple script runs; add child-log-lines to script log table 2023-01-24 11:57:50 -06:00
f1ff53059f Handle null case in convertInputIdsToEnumIdType 2023-01-24 11:08:44 -06:00
18a8656ba2 Fix enum looks by ids of wrong type 2023-01-24 11:04:34 -06:00
e75b645639 Turning up logging 2023-01-20 15:14:10 -06:00
804efbd608 SPRINT-19: fixed xbar images 2023-01-20 13:13:29 -06:00
fe30d2654b update to implicity add a queryJoin, if a filter field calls out a table name that matches a join in the instance 2023-01-20 10:05:44 -06:00
97b803a86a add audit joins, move names to constants; fixes 2023-01-20 10:03:52 -06:00
2daa42aeb1 Merge branch 'feature/sprint-19' of github.com:Kingsrook/qqq into feature/sprint-19 2023-01-19 16:32:58 -06:00
4344fa472a Initial checkin 2023-01-19 16:31:18 -06:00
6bb810c1f4 Good exception for bad table name 2023-01-19 16:30:59 -06:00
d65af53090 Add some withers 2023-01-19 16:30:44 -06:00
ac6a7ba15a fix setValueIfTableHasField - should catch if field isn't found (helps tables w/o createDate work) 2023-01-19 16:12:30 -06:00
1c150e207a add nonNullArray and mergeLists 2023-01-19 16:12:02 -06:00
f9408716ac update to propagate default values into state before frontend steps 2023-01-19 16:11:45 -06:00
396f02265b explicit errors if context isn't setup 2023-01-19 16:11:19 -06:00
a1674792c6 Merge branch 'dev' into feature/sprint-19 2023-01-18 17:36:46 -06:00
22565b3ecd Initial checkin 2023-01-18 14:19:12 -06:00
d2e7b794f4 moving QLogger package 2023-01-18 12:11:40 -06:00
7000da409a moving standard lambdas to common package 2023-01-18 12:11:40 -06:00
3f0e09e32a Adding javalin access logs 2023-01-18 12:11:40 -06:00
70d9d259c1 Adding heavy field concept 2023-01-18 12:11:35 -06:00
4dc9c52ee0 Adding toLogPair method 2023-01-18 11:56:10 -06:00
964405d210 Initial checkin 2023-01-18 11:55:17 -06:00
369ba3c8d7 switch syslog to a json format (via patternLayout) 2023-01-18 11:46:12 -06:00
24d5406ee3 add filterStackTrace; move unsafeSupplier 2023-01-18 11:40:39 -06:00
984012f3a3 heavy fields, qvalueFormatter static 2023-01-18 11:39:47 -06:00
e8264d915f hotfix: updated to move pie chart label to be specified in the metacritic, added 'add' to pom resolver 2023-01-18 11:33:10 -06:00
46adfa8e24 WIP - logger migrations; initial work for data bag view widget 2023-01-17 10:44:45 -06:00
178078282c Switch to use QLogger everywhere 2023-01-17 10:44:45 -06:00
d03e947889 Merge branch 'dev' into feature/sprint-19 2023-01-17 10:29:16 -06:00
43de1cf749 Add post to download endpoint 2023-01-16 09:03:13 -06:00
d3fa1df56f Implementation of QContext everywhere, instead of passing QInstance and QSession in all ActionInputs 2023-01-15 19:41:23 -06:00
69a6104393 updated snapshot version, improved end of sprint script 2023-01-13 15:59:01 -06:00
e831c75e1e Merge branch 'dev' of github.com:Kingsrook/qqq into dev 2023-01-13 15:56:56 -06:00
eba9b0b4af Updating to <revision>0.11.0</revision> 2023-01-13 15:39:36 -06:00
9f82f35bcf Merge tag 'version-0.10.0' into dev
Tag release
2023-01-13 15:39:32 -06:00
ff07490f94 Merge branch 'release/0.10.0' 2023-01-13 15:38:34 -06:00
680696491f Update for next development version 2023-01-13 15:36:06 -06:00
a35e30059c Update versions for release 2023-01-13 15:36:05 -06:00
a37d22b0d0 Update export to work off post; add check for Authorization as a form param 2023-01-13 14:13:21 -06:00
9e02476ee7 Merge branch 'dev' 2023-01-13 11:02:59 -06:00
2b0974f4a5 Remove single-parent concept on app-children; more working version of recordLock from join 2023-01-13 09:42:31 -06:00
9a58c7683b SPRINT-18: updated module list to include slack and remove lambda 2023-01-12 21:07:55 -06:00
a556bf9764 SPRINT-18: added slack middleware module 2023-01-12 20:16:06 -06:00
41877a7055 Update to not setup a join context for non-table data sources 2023-01-12 08:38:09 -06:00
c07a77d4a6 Updated to disable view-all link if tablePath can't be found (e.g., because user can't view table) 2023-01-11 16:44:42 -06:00
23e9abeb74 implementation of record security locks, and permissions 2023-01-11 13:08:59 -06:00
e4d37e3db9 Add variant to pre-load by in-list, to cache misses as null 2023-01-05 14:33:32 -06:00
4b31a8b4bb Merge branch 'refs/heads/0.7.0-12-hotfix' into feature/sprint-18 2023-01-05 14:26:20 -06:00
d7e7315dc8 Merge branch 'feature/auth0-basic-auth-reuse' into feature/sprint-18 2023-01-05 11:36:39 -06:00
de05e4ae58 SPRINT-18: fixed unit test on auth0 reuse 2023-01-04 22:20:38 -06:00
150582964b Change setupSession to be public 2023-01-04 12:05:19 -06:00
2874b98b66 Add re-use of tokens from basicAuth 2023-01-04 10:07:44 -06:00
7fae3e2329 Add table-based authentication module; update javalin to support Authentication: Basic header; Move authentication classes 2022-12-28 17:00:08 -06:00
428f48602b Better testing on join reports, possible value translations; renamed left & right in QueryJoin (now joinTable, baseTable) 2022-12-22 13:46:32 -06:00
799b695e14 Checkpoint on report and export changes, possible value translating 2022-12-21 11:37:16 -06:00
19d88910b5 Updating table sync api 2022-12-20 10:56:59 -06:00
2ad4b22f55 Add withSchedule method 2022-12-19 15:02:20 -06:00
040dae55d5 Add withSchedule; fix return on withBasepullConfiguration; add overload withTransformStepClass 2022-12-19 14:59:58 -06:00
dd9253fde4 Updated comment about field behaviorfs 2022-12-19 14:56:37 -06:00
8a4d5bfb34 Update to not NPE if data source doesn't have a filter 2022-12-19 14:56:25 -06:00
e1c53b9d48 Updated interface in sync processes; more status updates in ETL processes; Basepull only update timestamp if ran as basepull; javalin report endpoint; 2022-12-19 12:04:01 -06:00
a6656af040 Updated for older api on QQueryFilter (noarg constructor) 2022-12-15 16:18:14 -06:00
0e68bf1e72 Hotfix - sqs batch mode, and rdbms delete-by-filter 2022-12-15 16:13:56 -06:00
1b672afcd0 Fixed rdbms delete test 2022-12-15 16:04:46 -06:00
5005c38c18 Fixes for performance of sqs (batch mode), plus bug in deleteAction by query-filter (with test that proves it) 2022-12-15 15:54:58 -06:00
1cdf437551 SPRINT-18: upped current snapshot version 2022-12-15 15:39:33 -06:00
04cddb8d5d Updating to <revision>0.9.0</revision> 2022-12-15 15:34:43 -06:00
0d6bd013f5 Merge branch 'release/0.9.0' 2022-12-15 15:33:49 -06:00
7b6284c06f Update for next development version 2022-12-15 15:24:00 -06:00
edcc27485e Update versions for release 2022-12-15 15:23:59 -06:00
280373ddc5 SPRINT-17: added code coverage test class to meet minimum requirements 2022-12-15 10:36:43 -06:00
25815ebc25 SPRINT-17: updated some widgets to look less broken when data is 'not available now', checkpoint commit on 'real dashboards' 2022-12-15 10:20:48 -06:00
59cbf83860 Add field increaseIsGood 2022-12-14 14:53:03 -06:00
6186b17e92 Add environmentValues to qInstance 2022-12-14 14:50:45 -06:00
30003b729c Add overload of getForeignRecordMap that takes additional (base) filter for query 2022-12-09 20:25:59 -06:00
293b3e4207 Add qruntime exception; let transform step set pipe capacity 2022-12-09 16:44:53 -06:00
470321dcf6 add equals and hashcode 2022-12-09 16:06:26 -06:00
48cfdeffa1 Change post-query customizer to be class (that can do list), not function 2022-12-09 12:05:39 -06:00
14c7fbe370 Moving dropdowns to work for all widgets 2022-12-09 09:53:13 -06:00
8454f94020 SPRINT-17: updated dropdowns to be required, added divider 2022-12-08 15:06:40 -06:00
697261f91b Add aHrefViewRecord 2022-12-08 10:37:11 -06:00
61d493a4f5 Add FieldValueList widget type, more html-helper methods 2022-12-08 09:18:52 -06:00
c21c89e85f SPRINT-17: test was not using groupby to fetch values 2022-12-07 16:43:27 -06:00
a9a3e3b19e SPRINT-17: fixed dumb checkstyle violation 2022-12-07 16:31:04 -06:00
5cf9e7b60d SPRINT-17: fixed failing test and removed pvs name member from parent meta data 2022-12-07 15:47:15 -06:00
9b34ee7fe7 SPRINT-17: updates to parent widget dropdown data, updated group bys to be objects allowing group by with custom formats 2022-12-07 15:31:48 -06:00
241741e2e5 Fix default for canAddChildRecord to be false; add more link-generating functions (to child-modals on view screens, etc) 2022-12-06 15:57:32 -06:00
a769d8942c Adding unique key check to insert action; adding post-insert customizer 2022-12-05 15:46:17 -06:00
c22fc89cbb SPRINT-17: changed some variable names 2022-12-05 12:18:23 -06:00
060da69afb Adding table-cacheOf concept; ability to add a child record from child-list widget 2022-12-05 10:26:53 -06:00
3691ad87e5 Merge remote-tracking branch 'origin/0.7.0-12-hotfix' into dev 2022-12-01 16:59:19 -06:00
583ac70563 Merge tag 'version-0.8.0' into dev
Tag release
2022-12-01 16:56:13 -06:00
28e0bdadbf Update for next development version 2022-12-01 16:49:34 -06:00
40afb629e7 Add overrideBatchSize to table automation details 2022-11-30 11:30:18 -06:00
449 changed files with 25847 additions and 3806 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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");
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
/////////////////////////////////////////////////////////////////////////////////////

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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
**

View File

@ -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;

View File

@ -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> &nbsp; " + value + "</li>");
return ("<li><b>" + name + "</b> &nbsp; " + 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>");
}
}

View File

@ -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));

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}
}

View File

@ -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()));
}
}

View File

@ -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)

View File

@ -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));
}

View File

@ -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)

View File

@ -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"))));
}
}

View File

@ -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());
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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()

View File

@ -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();
}
}

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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)
{

View File

@ -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);

View File

@ -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();
}

View File

@ -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));
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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);

View File

@ -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);

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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.
*******************************************************************************/

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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);
///////////////////////////////////////////////////////////////////////////////////

View File

@ -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);
}
}
}
}

View File

@ -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);

View File

@ -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)
{
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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)
{

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
{
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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
**

View File

@ -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;
}
}

View File

@ -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
**

View File

@ -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();
}

View File

@ -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)));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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));
}

View File

@ -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()));
}

View File

@ -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
**

View File

@ -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);

View File

@ -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
**

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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