Compare commits

..

143 Commits

Author SHA1 Message Date
30ecd2d331 Merge branch 'release/0.6.0' 2022-11-03 11:56:25 -05:00
11648a5759 Update versions for release 2022-11-03 11:45:58 -05:00
e433b78e5b Merge pull request #10 from Kingsrook/feature/sprint-14
Feature/sprint 14
2022-11-03 11:43:31 -05:00
ce9316453b PRDONE-136 - Adding support for basic auth login via auth0 2022-11-03 11:36:18 -05:00
ab0b38dd82 Add RunReportForRecordProcess; 1st version of AbstractProcessMetaDataBuilder 2022-11-03 10:29:44 -05:00
3c04841f73 sprint-14: initial checkin of api update action 2022-11-02 11:04:05 -05:00
683b3c658d Cleanup from code review 2022-11-01 16:21:30 -05:00
165583cd98 Initial version of scripts, javascript 2022-10-31 15:48:27 -05:00
0ada444fd4 sprint-14: moved 'final' before 'static' 2022-10-28 11:12:57 -05:00
662fefea19 sprint-14: put json data into backend details 2022-10-28 11:09:35 -05:00
1cdb4b37e9 sprint-14: fixed test which expected an instant but now receives a string 2022-10-27 13:22:14 -05:00
622183e276 sprint-14: moved getting of timestamps into their own methods that can be overridden by subclasses 2022-10-27 13:17:38 -05:00
80af7a3710 Add RunProcessAction.BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD 2022-10-27 11:17:08 -05:00
e6db303bc2 Fix last build 2022-10-27 11:12:27 -05:00
da116349ff Update to only update basepull timestamp after execution; make sure preRun runs before count 2022-10-27 11:02:47 -05:00
5e7b6f40df Add path to icon; other cleanup 2022-10-26 18:09:07 -05:00
82810c2b66 Add ExtractViaBasepullQueryStep; add pagination & piping to api query 2022-10-26 12:31:09 -05:00
77927fd318 sprint-14: minor updates to allow flexibility when extending loadviainsertorupdatestep 2022-10-26 11:56:17 -05:00
033dbeb76c Update to run an executeStep pre-action 2022-10-25 13:31:48 -05:00
8ffc1c1a63 udpated api json parsing (lenient mode); add escaping table names in rdbms 2022-10-25 10:47:06 -05:00
dae803f709 Initial checkin of QueryStringBuilder 2022-10-25 09:55:28 -05:00
128c379f10 sprint-14: initial checkin of basepull capability on processes 2022-10-24 17:06:11 -05:00
44537e182d Add dedicated method for api count in baseApiActionUtil; improve null handling in json to record 2022-10-24 15:34:37 -05:00
22b2e01cca Add withSectionOfFields 2022-10-24 08:40:04 -05:00
f5f6446069 Add GET action, and usage in API 2022-10-21 14:40:38 -05:00
204d67dd21 Merge branch 'main' into dev
# Conflicts:
#	qqq-backend-core/pom.xml
2022-10-21 13:42:30 -05:00
234ec4823b Merge pull request #8 from Kingsrook/feature/sprint-11
Feature/sprint 11
2022-10-21 12:21:40 -05:00
fa2ab18e30 Add unique keys, and checking of them in bulk load; add some more validation (sqs and unique keys) 2022-10-21 11:23:30 -05:00
20c42deae5 Fix where automation status got set to OK instead of running; switch to do an automation polling thread per-table/status 2022-10-20 10:48:58 -05:00
8b3b300eb1 Add custom values to api backend meta data 2022-10-19 18:02:35 -05:00
18a3f72c4a updated api backend to support count and query 2022-10-19 10:43:39 -05:00
bf3835bd4c Set to 0 covered ratio 2022-10-19 10:35:19 -05:00
84ccd92a6e Add rate-limit records to output 2022-10-19 10:33:01 -05:00
0c37630754 Disablign in CI 2022-10-19 10:32:51 -05:00
98e846d1f1 add try-catch around value casting 2022-10-19 09:27:41 -05:00
b12de62295 Fix bulk process PVS rendering 2022-10-19 09:09:44 -05:00
bb975e7c6a Merge pull request #9 from Kingsrook/dependabot/maven/qqq-backend-core/com.fasterxml.jackson.core-jackson-databind-2.13.4.1
Bump jackson-databind from 2.13.2.1 to 2.13.4.1 in /qqq-backend-core
2022-10-19 08:40:31 -05:00
8423a8db2e Bump jackson-databind from 2.13.2.1 to 2.13.4.1 in /qqq-backend-core
Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.13.2.1 to 2.13.4.1.
- [Release notes](https://github.com/FasterXML/jackson/releases)
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 06:42:19 +00:00
2d3d1091fd Add basic rate limit handling for POST 2022-10-18 11:12:25 -05:00
6345846eba Adding queues and queue providers; Adding schedules and ScheduleManager 2022-10-18 09:00:21 -05:00
888239b265 Refactored lambda to do standard qqq actions (once implemented) 2022-10-14 16:55:59 -05:00
e3cc2e63f6 Initial checkin of lambda module 2022-10-14 11:45:15 -05:00
dbfc2fe2d8 Initial checkin of lambda module 2022-10-14 11:07:43 -05:00
37fbfd1c7c Update to accept count filter as POST 2022-10-14 10:19:21 -05:00
117bb621ff Update to accept query & filter as POST 2022-10-14 10:11:53 -05:00
7c339b4e81 Add qqq-backend-module-api 2022-10-12 18:24:17 -05:00
471954e8b9 Initial checkin of API module 2022-10-12 18:00:08 -05:00
b91273a53a Add LoadViaInsertOrUpdateStep; make PVS field labels not have Id suffix; add populateFromQRecord 2022-10-11 16:37:33 -05:00
aa64f1b7f3 Update to only call consumer after the loop when it's smart to do so 2022-10-11 11:30:28 -05:00
6ec6b173b9 Updated to not try to prime PVS cache for non-table PVS's 2022-10-11 08:41:45 -05:00
c43a8998ec Updating to support possible value searching 2022-10-10 17:01:15 -05:00
262038bc87 Adding booleanOperator and subFilters to QQueryFilter 2022-10-10 09:13:16 -05:00
28060e95e3 Fixed NPE with null value from possible values 2022-10-07 12:24:27 -05:00
d0c26719e1 SPRINT-12: added test for Qbackend module interface 2022-10-07 10:19:23 -05:00
1548d7f35b Initial checkin 2022-10-07 10:14:35 -05:00
e53d559b29 SPRINT-12: added process step class validation, added noop transform step 2022-10-06 19:44:27 -05:00
73df50add1 Handle booleans better 2022-10-04 11:33:04 -05:00
c5a3534d43 Fix query bugs w/ in-list, empty; format booleans as Yes/No 2022-10-04 09:56:33 -05:00
e3903c0ab9 SPRINT-12: fixed broken test due to types on widgets now 2022-10-03 14:15:46 -05:00
ba4fddb7e5 SPRINT-12: fixed broken test due to types on widgets now 2022-10-03 14:07:10 -05:00
bb85d362fb SPRINT-12: added widget meta data 2022-10-03 14:02:55 -05:00
7415732c6c Update to translate possible values when building other possible values... 2022-10-03 13:31:13 -05:00
21cd07b2df Update scrubValues method to make instants out of DateTimes - fixes update actions in javalin apps 2022-10-03 10:40:45 -05:00
3f84271a36 Feedback from code reviews 2022-10-03 09:09:06 -05:00
17cace070c Change process summary line to interface; add record-link summary-line 2022-09-30 13:32:18 -05:00
456364de2a Add static data provider capability to reports 2022-09-29 16:55:08 -05:00
2d2eae5c06 Add rollback of transaction in last test 2022-09-29 14:39:43 -05:00
86ebe6ee4e Add transaction to transform step and query action (and rdbms query) 2022-09-29 14:34:51 -05:00
8a1110abf0 SPRINT-12: added stepper widget, added linkability to table widget 2022-09-29 12:04:24 -05:00
720200b6cf Update to clone query filters 2022-09-28 14:49:46 -05:00
70ded4c887 Add conversion of instant to localDate 2022-09-28 08:22:51 -05:00
89c9c72772 make all the method names not be the same 2022-09-27 19:05:10 -05:00
4d16bc0fc7 Auto-adorn PVS tables with link-to-record; change query to fetch instants, not LocalDateTimes 2022-09-27 18:59:58 -05:00
3587cc0676 Updated the update action, to set a null value for fields that came in the request as empty string 2022-09-27 14:14:46 -05:00
833b1f9643 SPRINT-12: added 'multi statitistics' widget 2022-09-27 14:14:36 -05:00
c002522cb8 Update to use column labels, when specified, in reports 2022-09-26 11:27:50 -05:00
3402a20b04 SPRINT-12: added rawHTML widget type 2022-09-26 10:18:25 -05:00
ac82928dd7 Do possible-values before display values, so a rendered possible-value can be part of a record label 2022-09-26 08:37:05 -05:00
d73e546c7b Add field adornments (links, chips, widths) 2022-09-23 17:01:46 -05:00
3ac6b34963 SPRINT-12: more tests fixed 2022-09-23 17:00:52 -05:00
742cf7fa3b SPRINT-12: fixed compile error due to merging 2022-09-23 16:26:13 -05:00
ce48933cbd SPRINT-12: fixed style issue and broken tests 2022-09-23 16:21:38 -05:00
5efd2da636 SPRINT-12: updated to define widgets at table level, refactoring of some of the widget stuff to match other "Action"s 2022-09-23 15:55:27 -05:00
3d07d215a9 Adding format date & time methods 2022-09-23 14:17:08 -05:00
9397934769 QQQ-42 adding data-sources to reports, customizer points 2022-09-23 09:55:34 -05:00
f83d2b3fc8 Merge branch 'QQQ-41-v-2-of-app-home-pages-dashboards-etc' into feature/sprint-11 2022-09-20 15:20:05 -05:00
d8c6bba6b4 QQQ-41: checkpoint commit for moving to next sprint 2022-09-20 15:18:49 -05:00
d5a319c458 Merge branch 'feature/QQQ-42-reports' into feature/sprint-11 2022-09-20 12:49:37 -05:00
1f546d8c7d QQQ-42 checkpoint of qqq reports 2022-09-20 12:46:32 -05:00
197dec3105 Merge remote-tracking branch 'origin/QQQ-41-v-2-of-app-home-pages-dashboards-etc' into feature/QQQ-42-reports 2022-09-19 13:52:54 -05:00
525389e62e QQQ-42 checkpoint of qqq reports 2022-09-19 13:52:43 -05:00
80ff7a26e0 QQQ-41: fixed failing test 2022-09-16 11:01:26 -05:00
0b10717116 Merge branch 'QQQ-41-v-2-of-app-home-pages-dashboards-etc' into feature/sprint-11 2022-09-16 10:51:24 -05:00
8891b3e7ea QQQ-41: added app sections, new widgets, ability to have no tables under app, etc. 2022-09-16 10:19:06 -05:00
01afdaacfd More live templates 2022-09-14 14:03:29 -05:00
112c62d035 Initial checkin 2022-09-14 13:59:34 -05:00
1e1c07958b Merge branch 'feature/QQQ-42-reports' into feature/sprint-11 2022-09-14 13:12:13 -05:00
b05c5749b4 QQQ-42 initial implementation of qqq reports (pivots, WIP) 2022-09-14 13:05:31 -05:00
247e419e07 Updated for new assumptions re: fieldSection labels 2022-09-09 15:19:57 -05:00
2f787036d0 Update to set QFieldSection label from name; try to avoid backend-module-not-defined errors 2022-09-09 15:13:28 -05:00
a1f5e90106 Initial import from qqq-dev-tools standalone repo 2022-09-09 15:00:20 -05:00
bee8a7a2d9 Merge tag 'version-0.5.0' into dev
Tag release
2022-09-08 11:18:10 -05:00
d34555cfb2 Merge branch 'release/0.5.0' 2022-09-08 11:17:25 -05:00
9a95fe36e6 Update for next development version 2022-09-08 11:12:56 -05:00
79607a8cac Update versions for release 2022-09-08 11:12:55 -05:00
278910e4ee Merge pull request #7 from Kingsrook/feature/sprint-10
Feature/sprint 10
2022-09-08 11:09:43 -05:00
0d0a7aec1c Initial checkin 2022-09-08 10:58:21 -05:00
b01758879c QQQ-37 Redo bulk processes in streamed-etl mode 2022-09-07 16:59:42 -05:00
1c75df3a09 added initial version of branding as metadata 2022-09-06 19:00:12 -05:00
25c9376ce4 QQQ-37 update streamed-etl steps to not have to use different record-list 2022-09-06 15:15:11 -05:00
31e6bf4d49 PRDONE-94: updates from code review feedback added .env test 2022-09-06 11:41:25 -05:00
12925127b2 Feedback from code reviews 2022-09-06 09:29:24 -05:00
9a8b49f1a7 Feedback from code reviews 2022-09-05 09:47:43 -05:00
4af7757fdd Merge commit '3a69ce7' into feature/sprint-10 2022-09-01 15:59:50 -05:00
91ba6f4b4e Merge branch 'feature/sprint-10' of github.com:Kingsrook/qqq into feature/sprint-10 2022-09-01 15:59:19 -05:00
3a69ce7d2f QQQ-40 getting closer to production-ready on automations 2022-09-01 15:57:01 -05:00
64e801747f QQQ-40 getting closer to production-ready on automations 2022-09-01 15:53:35 -05:00
6b4417d3e8 Merge branch 'feature/QQQ-38-app-home-widgets' into feature/sprint-10 2022-08-31 15:59:32 -05:00
e157809b35 Merge branch 'feature/QQQ-40-record-automations' into feature/sprint-10 2022-08-31 15:12:54 -05:00
a08ec0ae6f QQQ-40 Initial working POC 2022-08-31 15:01:45 -05:00
f08ffe691f PRDONE-94: updated to use interpreter for getting environment credentials, updated interpreter to load Dotenv files as environment overrides 2022-08-31 12:05:35 -05:00
69b9ed5b19 QQQ-37 adding pre & post run to ETL transform & load; minor QOL 2022-08-30 13:44:34 -05:00
4bf1fe8638 PRDONE-94: updated to look for dotenv properties and fall back to environment vars 2022-08-30 13:33:13 -05:00
dcea96579c PRDONE-94: updated to set dotenv variables as system properties 2022-08-30 12:28:40 -05:00
4c2ebf8a94 PRDONE-94: updated to ignore missing .env file 2022-08-30 12:17:37 -05:00
4316b47916 fixed incorrect import order 2022-08-30 12:13:29 -05:00
48b8d295e3 initial checkin of quicksight dashboard widget POC, updated to remove hard coded credentials 2022-08-30 11:46:46 -05:00
6142b8e703 Update tests to run w/ h2 instead of mysql 2022-08-29 14:29:01 -05:00
bc95899a50 Increase coverage 2022-08-29 14:20:00 -05:00
9106b82560 Fixing tests 2022-08-29 14:12:09 -05:00
39f065e23e Fixing tests 2022-08-29 13:52:54 -05:00
cb22f86793 Checkpoint - working versions of streamed with frontend processes, with validation 2022-08-29 13:33:35 -05:00
d32538bf45 Merge branch 'dev' into feature/QQQ-38-app-home-widgets 2022-08-25 10:14:26 -05:00
5f8f063b99 Merge branch 'dev' into feature/QQQ-37-streamed-processes 2022-08-25 10:09:23 -05:00
c3bba7cf8c Merge tag 'version-0.4.0' into dev
Tag release
2022-08-25 10:08:05 -05:00
103d229c82 Update for next development version 2022-08-25 10:01:52 -05:00
7355a9c8aa QQQ-37 Updating test coverage 2022-08-24 10:18:59 -05:00
c2972cd4df QQQ-37 checkpoint 2022-08-23 16:54:36 -05:00
834b4136de Merge branch 'feature/sprint-9-support-updates' into feature/QQQ-37-streamed-processes 2022-08-23 11:17:45 -05:00
459a533f60 upated to work in CI (e.g., w/o database) 2022-08-22 10:53:51 -05:00
937304e7f1 QQQ-38 Initial build of app home page widgets 2022-08-22 10:48:55 -05:00
21d66cc7fc QQQ-37 added test coverage for StreamedETLWithFrontendProcess 2022-08-22 10:08:27 -05:00
a86f42f373 QQQ-37 initial buidout of StreamedETLWithFrontendProcess 2022-08-22 08:36:09 -05:00
373 changed files with 46683 additions and 2170 deletions

3
.gitignore vendored
View File

@ -2,6 +2,7 @@ target/
*.iml
.env
.idea
#############################################
## Original contents from github template: ##
@ -29,6 +30,8 @@ target/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.DS_Store
*.swp
.flattened-pom.xml
dependency-reduced-pom.xml

View File

@ -179,9 +179,7 @@
<property name="ignoreFinal" value="false"/>
<property name="allowedAbbreviationLength" value="1"/>
</module>
-->
<module name="OverloadMethodsDeclarationOrder"/>
<!--
<module name="VariableDeclarationUsageDistance"/>
<module name="CustomImportOrder">
<property name="sortImportsInGroupAlphabetically" value="true"/>
@ -261,5 +259,9 @@
<module name="MissingOverride"/>
</module>
<module name="Header">
<property name="headerFile" value="checkstyle/license.txt"/>
<property name="fileExtensions" value="java"/>
</module>
<module name="SuppressWarningsFilter"/>
</module>

20
checkstyle/license.txt Normal file
View File

@ -0,0 +1,20 @@
/*
* 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/>.
*/

29
pom.xml
View File

@ -30,15 +30,19 @@
<modules>
<module>qqq-backend-core</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-backend-module-api</module>
<module>qqq-backend-module-filesystem</module>
<module>qqq-backend-module-rdbms</module>
<module>qqq-language-support-javascript</module>
<module>qqq-middleware-picocli</module>
<module>qqq-middleware-javalin</module>
<module>qqq-middleware-lambda</module>
<module>qqq-utility-lambdas</module>
<module>qqq-sample-project</module>
</modules>
<properties>
<revision>0.4.0</revision>
<revision>0.6.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -49,8 +53,20 @@
<coverage.haltOnFailure>true</coverage.haltOnFailure>
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase>
</properties>
<profiles>
<profile>
<!-- For qqq-middleware-lambda - to build its shaded jar, its qqq dependencies also need
to build a shaded jar. So to activate that mode, use this profile (-P buildShadedJar)-->
<id>buildShadedJar</id>
<properties>
<plugin.shade.phase>package</plugin.shade.phase>
</properties>
</profile>
</profiles>
<dependencyManagement>
<dependencies>
<dependency>
@ -74,6 +90,12 @@
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
@ -122,7 +144,8 @@
<id>validate</id>
<phase>validate</phase>
<configuration>
<configLocation>checkstyle.xml</configLocation>
<configLocation>checkstyle/config.xml</configLocation>
<headerLocation>checkstyle/license.txt</headerLocation>
<!-- <suppressionsLocation>checkstyle-suppressions.xml</suppressionsLocation> -->
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>

View File

@ -36,15 +36,30 @@
<!-- noe at this time -->
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.17.259</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- other qqq modules deps -->
<!-- none, this is core. -->
<!-- 3rd party deps specifically for this module -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>quicksight</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2.1</version>
<version>2.14.0-rc1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
@ -83,6 +98,29 @@
<version>5.2.2</version>
</dependency>
<!-- the next 3 deps are being added for google drive support -->
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>1.35.2</version>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-drive</artifactId>
<version>v3-rev20220815-2.0.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-sqs</artifactId>
<version>1.12.321</version>
</dependency>
<!-- Common deps for all qqq modules -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
@ -101,6 +139,11 @@
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
@ -121,6 +164,30 @@
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>${plugin.shade.phase}</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -22,6 +22,9 @@
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.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
@ -48,4 +51,16 @@ public class ActionHelper
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void editFirstValue(List<Serializable> values, Function<String, String> editFunction)
{
if(values.size() > 0)
{
values.set(0, editFunction.apply(String.valueOf(values.get(0))));
}
}
}

View File

@ -31,7 +31,6 @@ import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
** Argument passed to an AsyncJob when it runs, which can be used to communicate
** data back out of the job.
**
** TODO - future - allow cancellation to be indicated here?
*******************************************************************************/
public class AsyncJobCallback
{
@ -51,6 +50,17 @@ public class AsyncJobCallback
/*******************************************************************************
** Setter for asyncJobStatus
**
*******************************************************************************/
public void setAsyncJobStatus(AsyncJobStatus asyncJobStatus)
{
this.asyncJobStatus = asyncJobStatus;
}
/*******************************************************************************
** Update the message
*******************************************************************************/
@ -107,4 +117,17 @@ public class AsyncJobCallback
AsyncJobManager.getStateProvider().put(new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS), asyncJobStatus);
}
/*******************************************************************************
** Check if the asyncJobStatus had a cancellation requested.
**
** TODO - concern about multiple threads writing this object to a non-in-memory
** state provider, and this value getting lost...
*******************************************************************************/
public boolean wasCancelRequested()
{
return (this.asyncJobStatus.getCancelRequested());
}
}

View File

@ -135,11 +135,11 @@ public class AsyncJobManager
Thread.currentThread().setName("Job:" + jobName + ":" + uuidAndTypeStateKey.getUuid().toString().substring(0, 8));
try
{
LOG.info("Starting job " + uuidAndTypeStateKey.getUuid());
LOG.debug("Starting job " + uuidAndTypeStateKey.getUuid());
T result = asyncJob.run(new AsyncJobCallback(uuidAndTypeStateKey.getUuid(), asyncJobStatus));
asyncJobStatus.setState(AsyncJobState.COMPLETE);
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
LOG.info("Completed job " + uuidAndTypeStateKey.getUuid());
LOG.debug("Completed job " + uuidAndTypeStateKey.getUuid());
return (result);
}
catch(Exception e)
@ -183,4 +183,15 @@ public class AsyncJobManager
// return TempFileStateProvider.getInstance();
}
/*******************************************************************************
**
*******************************************************************************/
public void cancelJob(String jobUUID)
{
Optional<AsyncJobStatus> jobStatus = getJobStatus(jobUUID);
jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true));
}
}

View File

@ -37,6 +37,8 @@ public class AsyncJobStatus implements Serializable
private Integer total;
private Exception caughtException;
private boolean cancelRequested;
/*******************************************************************************
@ -163,4 +165,26 @@ public class AsyncJobStatus implements Serializable
{
this.caughtException = caughtException;
}
/*******************************************************************************
** Getter for cancelRequested
**
*******************************************************************************/
public boolean getCancelRequested()
{
return cancelRequested;
}
/*******************************************************************************
** Setter for cancelRequested
**
*******************************************************************************/
public void setCancelRequested(boolean cancelRequested)
{
this.cancelRequested = cancelRequested;
}
}

View File

@ -0,0 +1,206 @@
/*
* 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.async;
import java.io.Serializable;
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.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Class that knows how to Run an asynchronous job (lambda, supplier) that writes into a
** RecordPipe, with another lambda (consumer) that consumes records from the pipe.
**
** Takes care of the job status monitoring, blocking when the pipe is empty, etc.
*******************************************************************************/
public class AsyncRecordPipeLoop
{
private static final Logger LOG = LogManager.getLogger(AsyncRecordPipeLoop.class);
private static final int TIMEOUT_AFTER_NO_RECORDS_MS = 10 * 60 * 1000;
private static final int MAX_SLEEP_MS = 1000;
private static final int INIT_SLEEP_MS = 10;
private static final int MIN_RECORDS_TO_CONSUME = 10;
/*******************************************************************************
** Run an async-record-pipe-loop.
**
** @param jobName name for the async job thread
** @param recordLimit optionally, cancel the supplier/job after this number of records.
* e.g., for a preview step.
** @param recordPipe constructed before this call, and used in both of the lambdas
** @param supplier lambda that adds records into the pipe.
* e.g., a query or extract step.
** @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
{
///////////////////////////////////////////////////
// start the extraction function as an async job //
///////////////////////////////////////////////////
AsyncJobManager asyncJobManager = new AsyncJobManager();
String jobUUID = asyncJobManager.startJob(jobName, supplier::apply);
LOG.debug("Started supplier job [" + jobUUID + "] for record pipe.");
AsyncJobState jobState = AsyncJobState.RUNNING;
AsyncJobStatus asyncJobStatus = null;
int recordCount = 0;
int nextSleepMillis = INIT_SLEEP_MS;
long lastReceivedRecordsAt = System.currentTimeMillis();
long jobStartTime = System.currentTimeMillis();
boolean everCalledConsumer = false;
while(jobState.equals(AsyncJobState.RUNNING))
{
if(recordPipe.countAvailableRecords() < MIN_RECORDS_TO_CONSUME)
{
///////////////////////////////////////////////////////////////
// if the pipe is too empty, sleep to let the producer work. //
// todo - smarter sleep? like get notified vs. sleep? //
///////////////////////////////////////////////////////////////
LOG.trace("Too few records are available in the pipe. Sleeping [" + nextSleepMillis + "] ms to give producer a chance to work");
SleepUtils.sleep(nextSleepMillis, TimeUnit.MILLISECONDS);
nextSleepMillis = Math.min(nextSleepMillis * 2, MAX_SLEEP_MS);
long timeSinceLastReceivedRecord = System.currentTimeMillis() - lastReceivedRecordsAt;
if(timeSinceLastReceivedRecord > TIMEOUT_AFTER_NO_RECORDS_MS)
{
throw (new QException("Job appears to have stopped producing records (last record received " + timeSinceLastReceivedRecord + " ms ago)."));
}
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the pipe has records, consume them. reset the sleep timer so if we sleep again it'll be short. //
////////////////////////////////////////////////////////////////////////////////////////////////////////
lastReceivedRecordsAt = System.currentTimeMillis();
nextSleepMillis = INIT_SLEEP_MS;
everCalledConsumer = true;
recordCount += consumer.get();
LOG.debug(String.format("Processed %,d records so far", recordCount));
if(recordLimit != null && recordCount >= recordLimit)
{
asyncJobManager.cancelJob(jobUUID);
////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the extract function doesn't recognize the cancellation request, //
// tell the pipe to "terminate" - meaning - flush its queue and just noop when given new records. //
// this should prevent anyone writing to such a pipe from potentially filling & blocking. //
////////////////////////////////////////////////////////////////////////////////////////////////////
recordPipe.terminate();
break;
}
}
//////////////////////////////
// refresh the job's status //
//////////////////////////////
Optional<AsyncJobStatus> optionalAsyncJobStatus = asyncJobManager.getJobStatus(jobUUID);
if(optionalAsyncJobStatus.isEmpty())
{
/////////////////////////////////////////////////
// todo - ... maybe some version of try-again? //
/////////////////////////////////////////////////
throw (new QException("Could not get status of job [" + jobUUID + "]"));
}
asyncJobStatus = optionalAsyncJobStatus.get();
jobState = asyncJobStatus.getState();
}
LOG.debug("Job [" + jobUUID + "][" + jobName + "] completed with status: " + asyncJobStatus);
///////////////////////////////////
// propagate errors from the job //
///////////////////////////////////
if(asyncJobStatus != null && asyncJobStatus.getState().equals(AsyncJobState.ERROR))
{
throw (new QException("Job failed with an error", asyncJobStatus.getCaughtException()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// send the final records to the consumer //
// note - we'll only make this "final" call to the consumer if: //
// - there are currently records in the pipe //
// - OR we never called the consumer (e.g., there were 0 rows produced by the supplier //
// This prevents cases where a consumer may get pages of records in the loop, but then //
// be called here post-loop w/ 0 records, and may interpret it as a sign that no records //
// were ever supplied. //
///////////////////////////////////////////////////////////////////////////////////////////
if(recordPipe.countAvailableRecords() > 0 || !everCalledConsumer)
{
recordCount += consumer.get();
}
long endTime = System.currentTimeMillis();
if(recordCount > 0)
{
LOG.info(String.format("Processed %,d records", recordCount)
+ String.format(" at end of job [%s] in %,d ms (%.2f records/second).", jobName, (endTime - jobStartTime), 1000d * (recordCount / (.001d + (endTime - jobStartTime)))));
}
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,115 @@
/*
* 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.automation;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
** enum of possible values for a record's Automation Status.
*******************************************************************************/
public enum AutomationStatus implements PossibleValueEnum<Integer>
{
PENDING_INSERT_AUTOMATIONS(1, "Pending Insert Automations"),
RUNNING_INSERT_AUTOMATIONS(2, "Running Insert Automations"),
FAILED_INSERT_AUTOMATIONS(3, "Failed Insert Automations"),
PENDING_UPDATE_AUTOMATIONS(4, "Pending Update Automations"),
RUNNING_UPDATE_AUTOMATIONS(5, "Running Update Automations"),
FAILED_UPDATE_AUTOMATIONS(6, "Failed Update Automations"),
OK(7, "OK");
private final Integer id;
private final String label;
/*******************************************************************************
**
*******************************************************************************/
AutomationStatus(int id, String label)
{
this.id = id;
this.label = label;
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return (id);
}
/*******************************************************************************
** Getter for label
**
*******************************************************************************/
public String getLabel()
{
return (label);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Integer getPossibleValueId()
{
return (getId());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getPossibleValueLabel()
{
return (getLabel());
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public String getInsertOrUpdate()
{
return switch(this)
{
case PENDING_INSERT_AUTOMATIONS, RUNNING_INSERT_AUTOMATIONS, FAILED_INSERT_AUTOMATIONS -> "Insert";
case PENDING_UPDATE_AUTOMATIONS, RUNNING_UPDATE_AUTOMATIONS, FAILED_UPDATE_AUTOMATIONS -> "Update";
case OK -> "";
};
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.automation;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
/*******************************************************************************
** Base class for custom-codes to run as an automation action
*******************************************************************************/
public abstract class RecordAutomationHandler
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(RecordAutomationInput recordAutomationInput) throws QException;
}

View File

@ -0,0 +1,166 @@
/*
* 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.automation;
import java.util.ArrayList;
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.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.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.utils.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Utility class for updating the automation status data for records
*******************************************************************************/
public class RecordAutomationStatusUpdater
{
private static final Logger LOG = LogManager.getLogger(RecordAutomationStatusUpdater.class);
/*******************************************************************************
** for a list of records from a table, set their automation status - based on
** how the table is configured.
*******************************************************************************/
public static boolean setAutomationStatusInRecords(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
{
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
{
return (false);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// In case an automation is running, and it updates records - don't let those records be marked //
// as PENDING_UPDATE_AUTOMATIONS... this is meant to avoid having a record's automation update //
// itself, and then continue to do so in a loop (infinitely). //
// BUT - shouldn't this be allowed to update OTHER records to be pending updates? It seems like //
// yes - so -that'll probably be a bug to fix at some point in the future todo //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{
Exception e = new Exception();
for(StackTraceElement stackTraceElement : e.getStackTrace())
{
String className = stackTraceElement.getClassName();
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test"))
{
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
return (false);
}
}
}
if(canWeSkipPendingAndGoToOkay(table, automationStatus))
{
automationStatus = AutomationStatus.OK;
}
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails.getStatusTracking() != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
for(QRecord record : records)
{
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
// todo - another field - for the automation timestamp??
}
}
return (true);
}
/*******************************************************************************
** If a table has no automation actions defined for Insert (or Update), and we're
** being asked to set status to PENDING_INSERT (or PENDING_UPDATE), then just
** move the status straight to OK.
*******************************************************************************/
private static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
{
List<TableAutomationAction> tableActions = Objects.requireNonNullElse(table.getAutomationDetails().getActions(), new ArrayList<>());
if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS))
{
return tableActions.stream().noneMatch(a -> TriggerEvent.POST_INSERT.equals(a.getTriggerEvent()));
}
else if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{
return tableActions.stream().noneMatch(a -> TriggerEvent.POST_UPDATE.equals(a.getTriggerEvent()));
}
return (false);
}
/*******************************************************************************
** for a list of records, update their automation status and actually Update the
** backend as well.
*******************************************************************************/
public static void setAutomationStatusInRecordsAndUpdate(QInstance instance, QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus) throws QException
{
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
boolean didSetStatusField = setAutomationStatusInRecords(table, records, automationStatus);
if(didSetStatusField)
{
UpdateInput updateInput = new UpdateInput(instance);
updateInput.setSession(session);
updateInput.setTableName(table.getName());
/////////////////////////////////////////////////////////////////////////////////////
// build records with just their pkey & status field for this update, to avoid //
// changing other values (relies on assumption of Patch semantics in UpdateAction) //
/////////////////////////////////////////////////////////////////////////////////////
updateInput.setRecords(records.stream().map(r -> new QRecord()
.withTableName(r.getTableName())
.withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField()))
.withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList());
updateInput.setAreAllValuesBeingUpdatedTheSame(true);
new UpdateAction().execute(updateInput);
}
}
else
{
// todo - verify if this is valid as other types are built
throw (new NotImplementedException("Updating record automation status is not implemented for table [" + table + "], tracking type: "
+ (automationDetails == null ? "null" : automationDetails.getStatusTracking().getType())));
}
}
}

View File

@ -0,0 +1,403 @@
/*
* 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.automation.polling;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
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;
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.QueryInput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
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.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.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;
/*******************************************************************************
** Runnable for the Polling Automation Provider, that looks for records that
** need automations, and executes them.
**
** An instance of this class should be created for each table/automation-status
** - see the TableActions inner record for that definition, and the static
** getTableActions method that helps someone who wants to start these threads
** figure out which ones are needed.
*******************************************************************************/
public class PollingAutomationPerTableRunner implements Runnable
{
private static final Logger LOG = LogManager.getLogger(PollingAutomationPerTableRunner.class);
private final TableActions tableActions;
private final String name;
private QInstance instance;
private Supplier<QSession> sessionSupplier;
private static Map<TriggerEvent, AutomationStatus> triggerEventAutomationStatusMap = Map.of(
TriggerEvent.POST_INSERT, AutomationStatus.PENDING_INSERT_AUTOMATIONS,
TriggerEvent.POST_UPDATE, AutomationStatus.PENDING_UPDATE_AUTOMATIONS
);
private static Map<AutomationStatus, AutomationStatus> pendingToRunningStatusMap = Map.of(
AutomationStatus.PENDING_INSERT_AUTOMATIONS, AutomationStatus.RUNNING_INSERT_AUTOMATIONS,
AutomationStatus.PENDING_UPDATE_AUTOMATIONS, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS
);
private static Map<AutomationStatus, AutomationStatus> pendingToFailedStatusMap = Map.of(
AutomationStatus.PENDING_INSERT_AUTOMATIONS, AutomationStatus.FAILED_INSERT_AUTOMATIONS,
AutomationStatus.PENDING_UPDATE_AUTOMATIONS, AutomationStatus.FAILED_UPDATE_AUTOMATIONS
);
/*******************************************************************************
**
*******************************************************************************/
public record TableActions(String tableName, AutomationStatus status, List<TableAutomationAction> actions)
{
}
/*******************************************************************************
**
*******************************************************************************/
public static List<TableActions> getTableActions(QInstance instance, String providerName)
{
Map<String, Map<AutomationStatus, List<TableAutomationAction>>> workingTableActionMap = new HashMap<>();
List<TableActions> tableActionList = new ArrayList<>();
//////////////////////////////////////////////////////////////////////
// todo - share logic like this among any automation implementation //
//////////////////////////////////////////////////////////////////////
for(QTableMetaData table : instance.getTables().values())
{
if(table.getAutomationDetails() != null && providerName.equals(table.getAutomationDetails().getProviderName()))
{
///////////////////////////////////////////////////////////////////////////
// organize the table's actions by type //
// todo - in future, need user-defined actions here too (and refreshed!) //
///////////////////////////////////////////////////////////////////////////
for(TableAutomationAction action : table.getAutomationDetails().getActions())
{
AutomationStatus automationStatus = triggerEventAutomationStatusMap.get(action.getTriggerEvent());
workingTableActionMap.putIfAbsent(table.getName(), new HashMap<>());
workingTableActionMap.get(table.getName()).putIfAbsent(automationStatus, new ArrayList<>());
workingTableActionMap.get(table.getName()).get(automationStatus).add(action);
}
////////////////////////////////////////////
// convert the map to tableAction records //
////////////////////////////////////////////
for(Map.Entry<AutomationStatus, List<TableAutomationAction>> entry : workingTableActionMap.get(table.getName()).entrySet())
{
AutomationStatus automationStatus = entry.getKey();
List<TableAutomationAction> actionList = entry.getValue();
actionList.sort(Comparator.comparing(TableAutomationAction::getPriority));
tableActionList.add(new TableActions(table.getName(), automationStatus, actionList));
}
}
}
return (tableActionList);
}
/*******************************************************************************
**
*******************************************************************************/
public PollingAutomationPerTableRunner(QInstance instance, String providerName, Supplier<QSession> sessionSupplier, TableActions tableActions)
{
this.instance = instance;
this.sessionSupplier = sessionSupplier;
this.tableActions = tableActions;
this.name = providerName + ">" + tableActions.tableName() + ">" + tableActions.status().getInsertOrUpdate();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
Thread.currentThread().setName(name);
LOG.info("Running " + this.getClass().getSimpleName() + "[" + name + "]");
try
{
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession();
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status(), tableActions.actions());
}
catch(Exception e)
{
LOG.warn("Error running automations", e);
}
}
/*******************************************************************************
** 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
{
if(CollectionUtils.nullSafeIsEmpty(actions))
{
return;
}
LOG.debug(" 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();
asyncRecordPipeLoop.run("PollingAutomationRunner>Query>" + automationStatus, null, recordPipe, (status) ->
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session);
queryInput.setTableName(table.getName());
AutomationStatusTrackingType statusTrackingType = table.getAutomationDetails().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()))));
}
else
{
throw (new NotImplementedException("Automation Status Tracking type [" + statusTrackingType + "] is not yet implemented in here."));
}
queryInput.setRecordPipe(recordPipe);
return (new QueryAction().execute(queryInput));
}, () ->
{
List<QRecord> records = recordPipe.consumeAvailableRecords();
applyActionsToRecords(session, table, records, actions, automationStatus);
return (records.size());
}
);
}
/*******************************************************************************
** For a set of records that were found to be in a PENDING state - run all the
** table's actions against them - IF they are found to match the action's filter
** (assuming it has one - if it doesn't, then all records match).
*******************************************************************************/
private void applyActionsToRecords(QSession session, QTableMetaData table, List<QRecord> records, List<TableAutomationAction> actions, AutomationStatus automationStatus) throws QException
{
if(CollectionUtils.nullSafeIsEmpty(records))
{
return;
}
///////////////////////////////////////////////////
// mark the records as RUNNING their automations //
///////////////////////////////////////////////////
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToRunningStatusMap.get(automationStatus));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// foreach action - run it against the records (but only if they match the action's filter, if there is one) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
boolean anyActionsFailed = false;
for(TableAutomationAction action : actions)
{
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(session, table, matchingQRecords, action);
}
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
anyActionsFailed = true;
}
}
////////////////////////////////////////
// update status on all these records //
////////////////////////////////////////
if(anyActionsFailed)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, pendingToFailedStatusMap.get(automationStatus));
}
else
{
RecordAutomationStatusUpdater.setAutomationStatusInRecordsAndUpdate(instance, session, table, records, AutomationStatus.OK);
}
}
/*******************************************************************************
** For a given action, and a list of records - return a new list, of the ones
** which match the action's filter (if there is one - if not, then all match).
**
** Note that this WILL re-query the objects (ALWAYS - even if the action has no filter).
** This has the nice side effect of always giving fresh/updated records, despite having
** some cost.
**
** At one point, we considered just applying the filter using java-comparisons,
** but that will almost certainly give potentially different results than a true
** backend - e.g., just consider if the DB is case-sensitive for strings...
*******************************************************************************/
private List<QRecord> getRecordsMatchingActionFilter(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session);
queryInput.setTableName(table.getName());
QQueryFilter filter = new QQueryFilter();
/////////////////////////////////////////////////////////////////////////////////////////////////////
// copy filter criteria from the action's filter to a new filter that we'll run here. //
// Critically - don't modify the filter object on the action! as that object has a long lifespan. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(action.getFilter() != null)
{
if(action.getFilter().getCriteria() != null)
{
action.getFilter().getCriteria().forEach(filter::addCriteria);
}
if(action.getFilter().getOrderBys() != null)
{
action.getFilter().getOrderBys().forEach(filter::addOrderBy);
}
}
filter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList()));
/////////////////////////////////////////////////////////////////////////////////////////////
// always add order-by the primary key, to give more predictable/consistent results //
// todo - in future - if this becomes a source of slowness, make this a config to opt-out? //
/////////////////////////////////////////////////////////////////////////////////////////////
filter.addOrderBy(new QFilterOrderBy().withFieldName(table.getPrimaryKeyField()));
queryInput.setFilter(filter);
return (new QueryAction().execute(queryInput).getRecords());
}
/*******************************************************************************
** Finally, actually run action code against a list of known matching records.
*******************************************************************************/
private void applyActionToMatchingRecords(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
{
if(StringUtils.hasContent(action.getProcessName()))
{
/////////////////////////////////////////////////////////////////////////////////////////
// if the action has a process associated with it - run that process. //
// 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.setProcessName(action.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.setCallback(new QProcessCallback()
{
@Override
public QQueryFilter getQueryFilter()
{
List<Serializable> recordIds = records.stream().map(r -> r.getValueInteger(table.getPrimaryKeyField())).collect(Collectors.toList());
return (new QQueryFilter().withCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordIds)));
}
});
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
if(runProcessOutput.getException().isPresent())
{
throw (runProcessOutput.getException().get());
}
}
else if(action.getCodeReference() != null)
{
LOG.debug(" Executing action: [" + action.getName() + "] as code reference: " + action.getCodeReference());
RecordAutomationInput input = new RecordAutomationInput(instance);
input.setSession(session);
input.setTableName(table.getName());
input.setRecordList(records);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
recordAutomationHandler.execute(input);
}
}
/*******************************************************************************
** Getter for name
**
*******************************************************************************/
public String getName()
{
return name;
}
}

View File

@ -24,12 +24,15 @@ package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.Optional;
import java.util.function.Function;
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.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;
@ -97,6 +100,120 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T extends BackendStep> T getBackendStep(Class<T> expectedType, QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA BackendSteps are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((T) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("unchecked")
public static <T> T getAdHoc(Class<T> expectedType, QCodeReference codeReference)
{
if(codeReference == null)
{
return (null);
}
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA code references are supported at this time."));
}
try
{
Class<?> customizerClass = Class.forName(codeReference.getName());
return ((T) customizerClass.getConstructor().newInstance());
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
// as we'll want to validate all functions in the instance validator at startup time (and IT will throw //
// if it finds an invalid code reference //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
return (null);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static RecordAutomationHandler getRecordAutomationHandler(TableAutomationAction action) throws QException
{
try
{
QCodeReference codeReference = action.getCodeReference();
if(!codeReference.getCodeType().equals(QCodeType.JAVA))
{
///////////////////////////////////////////////////////////////////////////////////////
// todo - 1) support more languages, 2) wrap them w/ java Functions here, 3) profit! //
///////////////////////////////////////////////////////////////////////////////////////
throw (new IllegalArgumentException("Only JAVA customizers are supported at this time."));
}
Class<?> codeClass = Class.forName(codeReference.getName());
Object codeObject = codeClass.getConstructor().newInstance();
if(!(codeObject instanceof RecordAutomationHandler recordAutomationHandler))
{
throw (new QException("The supplied code [" + codeClass.getName() + "] is not an instance of RecordAutomationHandler"));
}
return (recordAutomationHandler);
}
catch(QException qe)
{
throw (qe);
}
catch(Exception e)
{
throw (new QException("Error getting record automation handler for action [" + action.getName() + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -121,4 +238,5 @@ public class QCodeLoader
throw (new QException("Error getting custom possible value provider for PVS [" + possibleValueSource.getName() + "]", e));
}
}
}

View File

@ -1,3 +1,24 @@
/*
* 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;

View File

@ -1,3 +1,24 @@
/*
* 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;
@ -24,6 +45,7 @@ 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());
})));

View File

@ -0,0 +1,50 @@
/*
* 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;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
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.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
/*******************************************************************************
** Base class for rendering qqq dashboard widgets
**
*******************************************************************************/
public abstract class AbstractWidgetRenderer
{
public static final QValueFormatter valueFormatter = new QValueFormatter();
public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mma").withZone(ZoneId.systemDefault());
public static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault());
/*******************************************************************************
**
*******************************************************************************/
public abstract RenderWidgetOutput render(RenderWidgetInput input) throws QException;
}

View File

@ -0,0 +1,104 @@
/*
* 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;
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.QuickSightChart;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QuickSightChartMetaData;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.quicksight.QuickSightClient;
import software.amazon.awssdk.services.quicksight.model.GenerateEmbedUrlForRegisteredUserRequest;
import software.amazon.awssdk.services.quicksight.model.GenerateEmbedUrlForRegisteredUserResponse;
import software.amazon.awssdk.services.quicksight.model.RegisteredUserDashboardEmbeddingConfiguration;
import software.amazon.awssdk.services.quicksight.model.RegisteredUserEmbeddingExperienceConfiguration;
/*******************************************************************************
** Widget implementation for amazon QuickSight charts
**
*******************************************************************************/
public class QuickSightChartRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
try
{
QuickSightChartMetaData quickSightMetaData = (QuickSightChartMetaData) input.getWidgetMetaData();
QuickSightClient quickSightClient = getQuickSightClient(quickSightMetaData);
final RegisteredUserEmbeddingExperienceConfiguration experienceConfiguration = RegisteredUserEmbeddingExperienceConfiguration.builder()
.dashboard(
RegisteredUserDashboardEmbeddingConfiguration.builder()
.initialDashboardId(quickSightMetaData.getDashboardId())
.build())
.build();
final GenerateEmbedUrlForRegisteredUserRequest generateEmbedUrlForRegisteredUserRequest = GenerateEmbedUrlForRegisteredUserRequest.builder()
.awsAccountId(quickSightMetaData.getAccountId())
.userArn(quickSightMetaData.getUserArn())
.experienceConfiguration(experienceConfiguration)
.build();
final GenerateEmbedUrlForRegisteredUserResponse generateEmbedUrlForRegisteredUserResponse = quickSightClient.generateEmbedUrlForRegisteredUser(generateEmbedUrlForRegisteredUserRequest);
String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl();
QWidget widget = new QuickSightChart(input.getWidgetMetaData().getName(), quickSightMetaData.getLabel(), embedUrl);
return (new RenderWidgetOutput(widget));
}
catch(Exception e)
{
throw (new QException("Error rendering widget", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private QuickSightClient getQuickSightClient(QuickSightChartMetaData metaData)
{
AwsBasicCredentials awsCredentials = AwsBasicCredentials.create(metaData.getAccessKey(), metaData.getSecretKey());
QuickSightClient amazonQuickSightClient = QuickSightClient.builder()
.credentialsProvider(StaticCredentialsProvider.create(awsCredentials))
.region(Region.of(metaData.getRegion()))
.build();
return (amazonQuickSightClient);
}
}

View File

@ -0,0 +1,49 @@
/*
* 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;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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;
/*******************************************************************************
** Class for loading widget implementation code and rendering of widgets
**
*******************************************************************************/
public class RenderWidgetAction
{
/*******************************************************************************
**
*******************************************************************************/
public RenderWidgetOutput execute(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference());
return (widgetRenderer.render(input));
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.interfaces;
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;
/*******************************************************************************
** Interface for the Get action.
**
*******************************************************************************/
public interface GetInterface
{
/*******************************************************************************
**
*******************************************************************************/
GetOutput execute(GetInput getInput) throws QException;
}

View File

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.actions.interfaces;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
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;
@ -32,19 +31,11 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
** Interface for the Insert action.
**
*******************************************************************************/
public interface InsertInterface
public interface InsertInterface extends QActionInterface
{
/*******************************************************************************
**
*******************************************************************************/
InsertOutput execute(InsertInput insertInput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default QBackendTransaction openTransaction(InsertInput insertInput) throws QException
{
return (new QBackendTransaction());
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.interfaces;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
/*******************************************************************************
**
*******************************************************************************/
public interface QActionInterface
{
/*******************************************************************************
**
*******************************************************************************/
default QBackendTransaction openTransaction(AbstractTableActionInput input) throws QException
{
return (new QBackendTransaction());
}
}

View File

@ -30,14 +30,20 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
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.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.utils.CollectionUtils;
/*******************************************************************************
@ -64,8 +70,10 @@ public class MetaDataAction
Map<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
for(Map.Entry<String, QTableMetaData> entry : metaDataInput.getInstance().getTables().entrySet())
{
tables.put(entry.getKey(), new QFrontendTableMetaData(entry.getValue(), false));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String tableName = entry.getKey();
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
tables.put(tableName, new QFrontendTableMetaData(backendForTable, entry.getValue(), false));
treeNodes.put(tableName, new AppTreeNode(entry.getValue()));
}
metaDataOutput.setTables(tables);
@ -80,6 +88,27 @@ public class MetaDataAction
}
metaDataOutput.setProcesses(processes);
//////////////////////////////////////
// map reports to frontend metadata //
//////////////////////////////////////
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()));
}
metaDataOutput.setReports(reports);
//////////////////////////////////////
// map widgets to frontend metadata //
//////////////////////////////////////
Map<String, QFrontendWidgetMetaData> widgets = new LinkedHashMap<>();
for(Map.Entry<String, QWidgetMetaDataInterface> entry : metaDataInput.getInstance().getWidgets().entrySet())
{
widgets.put(entry.getKey(), new QFrontendWidgetMetaData(entry.getValue()));
}
metaDataOutput.setWidgets(widgets);
///////////////////////////////////
// map apps to frontend metadata //
///////////////////////////////////
@ -89,11 +118,14 @@ public class MetaDataAction
apps.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue()));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
if(CollectionUtils.nullSafeHasContents(entry.getValue().getChildren()))
{
for(QAppChildMetaData child : entry.getValue().getChildren())
{
apps.get(entry.getKey()).addChild(new AppTreeNode(child));
}
}
}
metaDataOutput.setApps(apps);
////////////////////////////////////////////////
@ -109,6 +141,14 @@ public class MetaDataAction
}
metaDataOutput.setAppTree(appTree);
////////////////////////////////////
// add branding metadata if found //
////////////////////////////////////
if(metaDataInput.getInstance().getBranding() != null)
{
metaDataOutput.setBranding(metaDataInput.getInstance().getBranding());
}
// todo post-customization - can do whatever w/ the result if you want?
return metaDataOutput;

View File

@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -52,7 +53,8 @@ public class TableMetaDataAction
{
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
tableMetaDataOutput.setTable(new QFrontendTableMetaData(table, true));
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -39,10 +39,16 @@ public interface QProcessCallback
/*******************************************************************************
** Get the filter query for this callback.
*******************************************************************************/
QQueryFilter getQueryFilter();
default QQueryFilter getQueryFilter()
{
return (null);
}
/*******************************************************************************
** Get the field values for this callback.
*******************************************************************************/
Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields);
default Map<String, Serializable> getFieldValues(List<QFieldMetaData> fields)
{
return (null);
}
}

View File

@ -23,26 +23,43 @@ package com.kingsrook.qqq.backend.core.actions.processes;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
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.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
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.data.QRecord;
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;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
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;
@ -55,6 +72,15 @@ public class RunProcessAction
{
private static final Logger LOG = LogManager.getLogger(RunProcessAction.class);
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
public static final String BASEPULL_TIMESTAMP_FIELD = "basepullTimestampField";
////////////////////////////////////////////////////////////////////////////////////////////////
// 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";
/*******************************************************************************
@ -82,15 +108,40 @@ public class RunProcessAction
runProcessOutput.setProcessUUID(runProcessInput.getProcessUUID());
UUIDAndTypeStateKey stateKey = new UUIDAndTypeStateKey(UUID.fromString(runProcessInput.getProcessUUID()), StateType.PROCESS_STATUS);
ProcessState processState = primeProcessState(runProcessInput, stateKey);
ProcessState processState = primeProcessState(runProcessInput, stateKey, process);
/////////////////////////////////////////////////////////
// if process is 'basepull' style, keep track of 'now' //
/////////////////////////////////////////////////////////
BasepullConfiguration basepullConfiguration = process.getBasepullConfiguration();
if(basepullConfiguration != null)
{
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
persistLastRunTime(runProcessInput, process, basepullConfiguration);
}
// todo - custom routing
List<QStepMetaData> stepList = getAvailableStepList(process, runProcessInput);
try
{
String lastStepName = runProcessInput.getStartAfterStep();
STEP_LOOP:
for(QStepMetaData step : stepList)
while(true)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// always refresh the step list - as any step that runs can modify it (in the process state). //
// this is why we don't do a loop over the step list - as we'd get ConcurrentModificationExceptions. //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QStepMetaData> stepList = getAvailableStepList(processState, process, lastStepName);
if(stepList.isEmpty())
{
break;
}
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData)
{
////////////////////////////////////////////////////////////////
@ -100,13 +151,13 @@ public class RunProcessAction
{
case BREAK ->
{
LOG.info("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processState.setNextStepName(step.getName());
break STEP_LOOP;
}
case SKIP ->
{
LOG.info("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
LOG.trace("Skipping frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
//////////////////////////////////////////////////////////////////////
// much less error prone in case this code changes in the future... //
@ -116,7 +167,7 @@ public class RunProcessAction
}
case FAIL ->
{
LOG.info("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
LOG.trace("Throwing error for frontend step [" + step.getName() + "] in process [" + process.getName() + "] (as requested by caller)");
throw (new QException("Failing process at step " + step.getName() + " (as requested, to fail on frontend steps)"));
}
default -> throw new IllegalStateException("Unexpected value: " + runProcessInput.getFrontendStepBehavior());
@ -127,6 +178,7 @@ public class RunProcessAction
///////////////////////
// Run backend steps //
///////////////////////
LOG.debug("Running backend step [" + step.getName() + "] in process [" + process.getName() + "]");
runBackendStep(runProcessInput, process, runProcessOutput, stateKey, backendStepMetaData, process, processState);
}
else
@ -137,6 +189,15 @@ public class RunProcessAction
throw (new QException("Unsure how to run a step of type: " + step.getClass().getName()));
}
}
////////////////////////////////////////////////////////////////////////////////////
// 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))))
{
storeLastRunTime(runProcessInput, process, basepullConfiguration);
}
}
catch(QException qe)
{
@ -169,7 +230,7 @@ public class RunProcessAction
** 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).
*******************************************************************************/
ProcessState primeProcessState(RunProcessInput runProcessInput, UUIDAndTypeStateKey stateKey) throws QException
ProcessState primeProcessState(RunProcessInput runProcessInput, UUIDAndTypeStateKey stateKey, QProcessMetaData process) throws QException
{
Optional<ProcessState> optionalProcessState = loadState(stateKey);
if(optionalProcessState.isEmpty())
@ -177,11 +238,14 @@ public class RunProcessAction
if(runProcessInput.getStartAfterStep() == null)
{
///////////////////////////////////////////////////////////////////////////////////
// this is fine - it means its our first time running in the backend. //
// This condition (no state in state-provider, and no start-after-step) means //
// that we're starting a new process! Init the process state here, then //
// Go ahead and store the state that we have (e.g., w/ initial records & values) //
///////////////////////////////////////////////////////////////////////////////////
storeState(stateKey, runProcessInput.getProcessState());
optionalProcessState = Optional.of(runProcessInput.getProcessState());
ProcessState processState = runProcessInput.getProcessState();
processState.setStepList(process.getStepList().stream().map(QStepMetaData::getName).toList());
storeState(stateKey, processState);
optionalProcessState = Optional.of(processState);
}
else
{
@ -233,7 +297,17 @@ public class RunProcessAction
runBackendStepInput.setTableName(process.getTableName());
runBackendStepInput.setSession(runProcessInput.getSession());
runBackendStepInput.setCallback(runProcessInput.getCallback());
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
///////////////////////////////////////////////////////////////
// if 'basepull' values are in the inputs, add to step input //
///////////////////////////////////////////////////////////////
if(runProcessInput.getValues().containsKey(BASEPULL_LAST_RUNTIME_KEY))
{
runBackendStepInput.setBasepullLastRunTime((Instant) runProcessInput.getValues().get(BASEPULL_LAST_RUNTIME_KEY));
}
RunBackendStepOutput lastFunctionResult = new RunBackendStepAction().execute(runBackendStepInput);
storeState(stateKey, lastFunctionResult.getProcessState());
@ -249,41 +323,63 @@ public class RunProcessAction
/*******************************************************************************
** Get the list of steps which are eligible to run.
*******************************************************************************/
private List<QStepMetaData> getAvailableStepList(QProcessMetaData process, RunProcessInput runProcessInput)
private List<QStepMetaData> getAvailableStepList(ProcessState processState, QProcessMetaData process, String lastStep) throws QException
{
if(runProcessInput.getStartAfterStep() == null)
if(lastStep == null)
{
/////////////////////////////////////////////////////////////////////////////
// if the caller did not supply a 'startAfterStep', then use the full list //
/////////////////////////////////////////////////////////////////////////////
return (process.getStepList());
///////////////////////////////////////////////////////////////////////
// if the caller did not supply a 'lastStep', then use the full list //
///////////////////////////////////////////////////////////////////////
return (stepNamesToSteps(process, processState.getStepList()));
}
else
{
////////////////////////////////////////////////////////////////////////////////
// else, loop until the startAfterStep is found, and return the ones after it //
////////////////////////////////////////////////////////////////////////////////
boolean foundStartAfterStep = false;
List<QStepMetaData> rs = new ArrayList<>();
////////////////////////////////////////////////////////////////////////////
// else, loop until the 'lastStep' is found, and return the ones after it //
////////////////////////////////////////////////////////////////////////////
boolean foundLastStep = false;
List<String> validStepNames = new ArrayList<>();
for(QStepMetaData step : process.getStepList())
for(String stepName : processState.getStepList())
{
if(foundStartAfterStep)
if(foundLastStep)
{
rs.add(step);
validStepNames.add(stepName);
}
if(step.getName().equals(runProcessInput.getStartAfterStep()))
if(stepName.equals(lastStep))
{
foundStartAfterStep = true;
foundLastStep = true;
}
}
return (rs);
return (stepNamesToSteps(process, validStepNames));
}
}
/*******************************************************************************
**
*******************************************************************************/
private List<QStepMetaData> stepNamesToSteps(QProcessMetaData process, List<String> stepNames) throws QException
{
List<QStepMetaData> result = new ArrayList<>();
for(String stepName : stepNames)
{
QStepMetaData step = process.getStep(stepName);
if(step == null)
{
throw (new QException("Could not find a step named [" + stepName + "] in this process."));
}
result.add(step);
}
return (result);
}
/*******************************************************************************
** Load an instance of the appropriate state provider
**
@ -329,4 +425,129 @@ public class RunProcessAction
return (getStateProvider().get(ProcessState.class, stateKey));
}
/*******************************************************************************
** Insert or update the last runtime value for this basepull into the backend.
*******************************************************************************/
protected void storeLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
{
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
queryInput.setSession(runProcessInput.getSession());
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()
.withFieldName(basepullKeyFieldName)
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(basepullKeyValue))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
//////////////////////////////////////////
// get the runtime for this process run //
//////////////////////////////////////////
Instant newRunTime = (Instant) runProcessInput.getValues().get(BASEPULL_THIS_RUNTIME_KEY);
/////////////////////////////////////////////////
// update if found, otherwise insert new value //
/////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
///////////////////////////////////////////////////////////////////////////////
// update the basepull table with 'now' (which is before original query ran) //
///////////////////////////////////////////////////////////////////////////////
QRecord basepullRecord = queryOutput.getRecords().get(0);
basepullRecord.setValue(basepullLastRunTimeFieldName, newRunTime);
////////////
// update //
////////////
UpdateInput updateInput = new UpdateInput(runProcessInput.getInstance());
updateInput.setSession(runProcessInput.getSession());
updateInput.setTableName(basepullTableName);
updateInput.setRecords(List.of(basepullRecord));
new UpdateAction().execute(updateInput);
}
else
{
QRecord basepullRecord = new QRecord()
.withValue(basepullKeyFieldName, basepullKeyValue)
.withValue(basepullLastRunTimeFieldName, newRunTime);
////////////////////////////////
// insert new basepull record //
////////////////////////////////
InsertInput insertInput = new InsertInput(runProcessInput.getInstance());
insertInput.setSession(runProcessInput.getSession());
insertInput.setTableName(basepullTableName);
insertInput.setRecords(List.of(basepullRecord));
new InsertAction().execute(insertInput);
}
}
/*******************************************************************************
** Lookup the last runtime for this basepull, and set it (plus now) in the process's
** values.
*******************************************************************************/
protected void persistLastRunTime(RunProcessInput runProcessInput, QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
{
////////////////////////////////////////////////////////
// if these values were already computed, don't re-do //
////////////////////////////////////////////////////////
if(runProcessInput.getValue(BASEPULL_THIS_RUNTIME_KEY) != null)
{
return;
}
/////////////////////////////////////////////////////////////////////////////////////////////////
// store 'now', which will be used to update basepull record if process completes successfully //
/////////////////////////////////////////////////////////////////////////////////////////////////
Instant now = Instant.now();
runProcessInput.getValues().put(BASEPULL_THIS_RUNTIME_KEY, now);
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
queryInput.setSession(runProcessInput.getSession());
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()
.withFieldName(basepullKeyFieldName)
.withOperator(QCriteriaOperator.EQUALS)
.withValues(List.of(basepullKeyValue))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
///////////////////////////////////////////////////////////////////////////////////////////////////
// get the stored time, if not, default to 'now' unless a number of hours to offset was provided //
///////////////////////////////////////////////////////////////////////////////////////////////////
Instant lastRunTime = now;
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
QRecord basepullRecord = queryOutput.getRecords().get(0);
lastRunTime = ValueUtils.getValueAsInstant(basepullRecord.getValue(basepullLastRunTimeFieldName));
}
else if(basepullHoursBackForInitialTimestamp != null)
{
lastRunTime = lastRunTime.minus(basepullHoursBackForInitialTimestamp, ChronoUnit.HOURS);
}
runProcessInput.getValues().put(BASEPULL_LAST_RUNTIME_KEY, lastRunTime);
runProcessInput.getValues().put(BASEPULL_TIMESTAMP_FIELD, basepullConfiguration.getTimestampField());
}
}

View File

@ -0,0 +1,167 @@
/*
* 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.queues;
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.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.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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Class to poll an SQS queue, and run process code for each message found.
*******************************************************************************/
public class SQSQueuePoller implements Runnable
{
private static final Logger LOG = LogManager.getLogger(SQSQueuePoller.class);
///////////////////////////////////////////////
// todo - move these 2 to a "QBaseRunnable"? //
///////////////////////////////////////////////
private QInstance qInstance;
private Supplier<QSession> sessionSupplier;
private SQSQueueProviderMetaData queueProviderMetaData;
private QQueueMetaData queueMetaData;
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
try
{
BasicAWSCredentials credentials = new BasicAWSCredentials(queueProviderMetaData.getAccessKey(), queueProviderMetaData.getSecretKey());
final AmazonSQS sqs = AmazonSQSClientBuilder.standard()
.withRegion(queueProviderMetaData.getRegion())
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
String queueUrl = queueProviderMetaData.getBaseURL();
if(!queueUrl.endsWith("/"))
{
queueUrl += "/";
}
queueUrl += queueMetaData.getQueueName();
while(true)
{
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest();
receiveMessageRequest.setQueueUrl(queueUrl);
ReceiveMessageResult receiveMessageResult = sqs.receiveMessage(receiveMessageRequest);
if(receiveMessageResult.getMessages().isEmpty())
{
LOG.debug("0 messages received. Breaking.");
break;
}
LOG.debug(receiveMessageResult.getMessages().size() + " messages received. Processing.");
for(Message message : receiveMessageResult.getMessages())
{
String body = message.getBody();
RunProcessInput runProcessInput = new RunProcessInput(qInstance);
runProcessInput.setSession(sessionSupplier.get());
runProcessInput.setProcessName(queueMetaData.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.addValue("body", body);
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
/////////////////////////////////
// todo - what of exceptions?? //
/////////////////////////////////
String receiptHandle = message.getReceiptHandle();
sqs.deleteMessage(new DeleteMessageRequest(queueUrl, receiptHandle));
}
}
}
catch(Exception e)
{
LOG.warn("Error receiving SQS Message", e);
}
}
/*******************************************************************************
** Setter for queueProviderMetaData
**
*******************************************************************************/
public void setQueueProviderMetaData(SQSQueueProviderMetaData queueProviderMetaData)
{
this.queueProviderMetaData = queueProviderMetaData;
}
/*******************************************************************************
** Setter for queueMetaData
**
*******************************************************************************/
public void setQueueMetaData(QQueueMetaData queueMetaData)
{
this.queueMetaData = queueMetaData;
}
/*******************************************************************************
** Setter for qInstance
**
*******************************************************************************/
public void setQInstance(QInstance qInstance)
{
this.qInstance = qInstance;
}
/*******************************************************************************
** Setter for sessionSupplier
**
*******************************************************************************/
public void setSessionSupplier(Supplier<QSession> sessionSupplier)
{
this.sessionSupplier = sessionSupplier;
}
}

View File

@ -27,24 +27,25 @@ 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.model.actions.reporting.ReportInput;
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;
/*******************************************************************************
** CSV report format implementation
** CSV export format implementation
*******************************************************************************/
public class CsvReportStreamer implements ReportStreamerInterface
public class CsvExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(CsvReportStreamer.class);
private static final Logger LOG = LogManager.getLogger(CsvExportStreamer.class);
private final QRecordToCsvAdapter qRecordToCsvAdapter;
private ReportInput reportInput;
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
@ -54,7 +55,7 @@ public class CsvReportStreamer implements ReportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
public CsvReportStreamer()
public CsvExportStreamer()
{
qRecordToCsvAdapter = new QRecordToCsvAdapter();
}
@ -65,14 +66,14 @@ public class CsvReportStreamer implements ReportStreamerInterface
**
*******************************************************************************/
@Override
public void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
this.reportInput = reportInput;
this.exportInput = exportInput;
this.fields = fields;
table = reportInput.getTable();
outputStream = this.reportInput.getReportOutputStream();
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
writeReportHeaderRow();
writeTitleAndHeader();
}
@ -80,9 +81,16 @@ public class CsvReportStreamer implements ReportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
private void writeReportHeaderRow() throws QReportingException
private void writeTitleAndHeader() throws QReportingException
{
try
{
if(StringUtils.hasContent(exportInput.getTitleRow()))
{
outputStream.write((exportInput.getTitleRow() + "\n").getBytes(StandardCharsets.UTF_8));
}
if(exportInput.getIncludeHeaderRow())
{
int col = 0;
for(QFieldMetaData column : fields)
@ -94,6 +102,8 @@ public class CsvReportStreamer implements ReportStreamerInterface
outputStream.write(('"' + column.getLabel() + '"').getBytes(StandardCharsets.UTF_8));
}
outputStream.write('\n');
}
outputStream.flush();
}
catch(Exception e)
@ -108,21 +118,29 @@ public class CsvReportStreamer implements ReportStreamerInterface
**
*******************************************************************************/
@Override
public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord) throws QReportingException
{
try
{
String csv = qRecordToCsvAdapter.recordToCsv(table, qRecord, fields);
outputStream.write(csv.getBytes(StandardCharsets.UTF_8));
outputStream.flush(); // todo - less often?
}
return (qRecords.size());
}
catch(Exception e)
{
throw (new QReportingException("Error writing CSV report", e));
@ -131,6 +149,17 @@ public class CsvReportStreamer implements ReportStreamerInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addTotalsRow(QRecord record) throws QReportingException
{
writeRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,352 @@
/*
* 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.OutputStream;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.model.actions.reporting.ExportInput;
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.fields.DisplayFormat;
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;
/*******************************************************************************
** Excel export format implementation
*******************************************************************************/
public class ExcelExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ExcelExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private ExcelStylerInterface excelStylerInterface = new PlainExcelStyler();
private Map<String, String> excelCellFormats;
private Workbook workbook;
private Worksheet worksheet;
private int row = 0;
private int sheetCount = 0;
/*******************************************************************************
**
*******************************************************************************/
public ExcelExportStreamer()
{
}
/*******************************************************************************
** display formats is a map of field name to Excel format strings (e.g., $#,##0.00)
*******************************************************************************/
@Override
public void setDisplayFormats(Map<String, String> displayFormats)
{
this.excelCellFormats = new HashMap<>();
for(Map.Entry<String, String> entry : displayFormats.entrySet())
{
String excelFormat = DisplayFormat.getExcelFormat(entry.getValue());
if(excelFormat != null)
{
excelCellFormats.put(entry.getKey(), excelFormat);
}
}
}
/*******************************************************************************
** Starts a new worksheet in the current workbook. Can be called multiple times.
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
try
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
this.row = 0;
this.sheetCount++;
/////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is the first call in here (e.g., the workbook hasn't been opened yet), then open it now //
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(workbook == null)
{
String appName = "QQQ";
QInstance instance = exportInput.getInstance();
if(instance != null && instance.getBranding() != null && instance.getBranding().getCompanyName() != null)
{
appName = instance.getBranding().getCompanyName();
}
workbook = new Workbook(outputStream, appName, null);
}
/////////////////////////////////////////////////////////////////////////////////////
// if start is called a second time (e.g., and there's already an open worksheet), //
// finish that sheet, before a new one is created. //
/////////////////////////////////////////////////////////////////////////////////////
if(worksheet != null)
{
worksheet.finish();
}
worksheet = workbook.newWorksheet(Objects.requireNonNullElse(label, "Sheet" + sheetCount));
writeTitleAndHeader();
}
catch(Exception e)
{
throw (new QReportingException("Error starting worksheet", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeTitleAndHeader() throws QReportingException
{
try
{
///////////////
// title row //
///////////////
if(StringUtils.hasContent(exportInput.getTitleRow()))
{
worksheet.value(row, 0, exportInput.getTitleRow());
worksheet.range(row, 0, row, fields.size() - 1).merge();
StyleSetter titleStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleTitleRow(titleStyle);
titleStyle.set();
row++;
worksheet.flush();
}
////////////////
// header row //
////////////////
if(exportInput.getIncludeHeaderRow())
{
int col = 0;
for(QFieldMetaData column : fields)
{
worksheet.value(row, col, column.getLabel());
col++;
}
StyleSetter headerStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleHeaderRow(headerStyle);
headerStyle.set();
row++;
worksheet.flush();
}
}
catch(Exception e)
{
throw (new QReportingException("Error starting Excel report"));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
row++;
worksheet.flush(); // todo? not at all? or just sometimes?
}
}
catch(Exception e)
{
LOG.error("Exception generating excel file", e);
try
{
workbook.finish();
outputStream.close();
}
finally
{
throw (new QReportingException("Error generating Excel report", e));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord)
{
int col = 0;
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)
{
if(value instanceof String s)
{
worksheet.value(row, col, s);
}
else if(value instanceof Number n)
{
worksheet.value(row, col, n);
if(excelCellFormats != null)
{
String format = excelCellFormats.get(field.getName());
if(format != null)
{
worksheet.style(row, col).format(format).set();
}
}
}
else if(value instanceof Boolean b)
{
worksheet.value(row, col, b);
}
else if(value instanceof Date d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDate d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else if(value instanceof ZonedDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else if(value instanceof Instant i)
{
// todo - what would be a better zone to use here?
worksheet.value(row, col, i.atZone(ZoneId.systemDefault()));
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else
{
worksheet.value(row, col, ValueUtils.getValueAsString(value));
}
}
col++;
}
}
/*******************************************************************************
**
*******************************************************************************/
public void addTotalsRow(QRecord record)
{
writeRecord(record);
StyleSetter totalsRowStyle = worksheet.range(row, 0, row, fields.size() - 1).style();
excelStylerInterface.styleTotalsRow(totalsRowStyle);
totalsRowStyle.set();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish() throws QReportingException
{
try
{
if(workbook != null)
{
workbook.finish();
}
}
catch(Exception e)
{
throw (new QReportingException("Error finishing Excel report", e));
}
}
}

View File

@ -1,214 +0,0 @@
/*
* 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.OutputStream;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
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.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dhatim.fastexcel.Workbook;
import org.dhatim.fastexcel.Worksheet;
/*******************************************************************************
** Excel report format implementation
*******************************************************************************/
public class ExcelReportStreamer implements ReportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ExcelReportStreamer.class);
private ReportInput reportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private Workbook workbook;
private Worksheet worksheet;
private int row = 1;
/*******************************************************************************
**
*******************************************************************************/
public ExcelReportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException
{
this.reportInput = reportInput;
this.fields = fields;
table = reportInput.getTable();
outputStream = this.reportInput.getReportOutputStream();
workbook = new Workbook(outputStream, "QQQ", null);
worksheet = workbook.newWorksheet("Sheet 1");
writeReportHeaderRow();
}
/*******************************************************************************
**
*******************************************************************************/
private void writeReportHeaderRow() throws QReportingException
{
try
{
int col = 0;
for(QFieldMetaData column : fields)
{
worksheet.value(0, col, column.getLabel());
col++;
}
worksheet.flush();
}
catch(Exception e)
{
throw (new QReportingException("Error starting Excel report"));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException
{
List<QRecord> qRecords = recordPipe.consumeAvailableRecords();
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
try
{
for(QRecord qRecord : qRecords)
{
int col = 0;
for(QFieldMetaData column : fields)
{
Serializable value = qRecord.getValue(column.getName());
if(value != null)
{
if(value instanceof String s)
{
worksheet.value(row, col, s);
}
else if(value instanceof Number n)
{
worksheet.value(row, col, n);
}
else if(value instanceof Boolean b)
{
worksheet.value(row, col, b);
}
else if(value instanceof Date d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDate d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd").set();
}
else if(value instanceof LocalDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else if(value instanceof ZonedDateTime d)
{
worksheet.value(row, col, d);
worksheet.style(row, col).format("yyyy-MM-dd H:mm:ss").set();
}
else
{
worksheet.value(row, col, ValueUtils.getValueAsString(value));
}
}
col++;
}
row++;
worksheet.flush(); // todo? not at all? or just sometimes?
}
}
catch(Exception e)
{
try
{
workbook.finish();
outputStream.close();
}
finally
{
throw (new QReportingException("Error generating Excel report", e));
}
}
return (qRecords.size());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish() throws QReportingException
{
try
{
if(workbook != null)
{
workbook.finish();
}
}
catch(Exception e)
{
throw (new QReportingException("Error finishing Excel report", e));
}
}
}

View File

@ -35,12 +35,13 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
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.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.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.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
@ -53,7 +54,7 @@ import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Action to generate a report.
** Action to generate an export from a table
**
** At this time (future may change?), this action starts a new thread to run
** the query in the backend module. As records are produced by the query,
@ -63,9 +64,9 @@ import org.apache.logging.log4j.Logger;
** time the report outputStream can be closed.
**
*******************************************************************************/
public class ReportAction
public class ExportAction
{
private static final Logger LOG = LogManager.getLogger(ReportAction.class);
private static final Logger LOG = LogManager.getLogger(ExportAction.class);
private boolean preExecuteRan = false;
private Integer countFromPreExecute = null;
@ -82,21 +83,21 @@ public class ReportAction
** first, in their thread, to catch any validation errors before they start
** the thread (which they may abandon).
*******************************************************************************/
public void preExecute(ReportInput reportInput) throws QException
public void preExecute(ExportInput exportInput) throws QException
{
ActionHelper.validateSession(reportInput);
ActionHelper.validateSession(exportInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend());
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend());
///////////////////////////////////
// verify field names (if given) //
///////////////////////////////////
if(CollectionUtils.nullSafeHasContents(reportInput.getFieldNames()))
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
QTableMetaData table = reportInput.getTable();
QTableMetaData table = exportInput.getTable();
List<String> badFieldNames = new ArrayList<>();
for(String fieldName : reportInput.getFieldNames())
for(String fieldName : exportInput.getFieldNames())
{
try
{
@ -119,8 +120,8 @@ public class ReportAction
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if this report format has a max-rows limit -- if so, do a count to verify we're under the limit //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat = reportInput.getReportFormat();
verifyCountUnderMax(reportInput, backendModule, reportFormat);
ReportFormat reportFormat = exportInput.getReportFormat();
verifyCountUnderMax(exportInput, backendModule, reportFormat);
preExecuteRan = true;
}
@ -130,28 +131,28 @@ public class ReportAction
/*******************************************************************************
** Run the report.
*******************************************************************************/
public ReportOutput execute(ReportInput reportInput) throws QException
public ExportOutput execute(ExportInput exportInput) throws QException
{
if(!preExecuteRan)
{
/////////////////////////////////////
// ensure that pre-execute has ran //
/////////////////////////////////////
preExecute(reportInput);
preExecute(exportInput);
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(reportInput.getBackend());
QBackendModuleInterface backendModule = qBackendModuleDispatcher.getQBackendModule(exportInput.getBackend());
//////////////////////////
// set up a query input //
//////////////////////////
QueryInterface queryInterface = backendModule.getQueryInterface();
QueryInput queryInput = new QueryInput(reportInput.getInstance());
queryInput.setSession(reportInput.getSession());
queryInput.setTableName(reportInput.getTableName());
queryInput.setFilter(reportInput.getQueryFilter());
queryInput.setLimit(reportInput.getLimit());
QueryInput queryInput = new QueryInput(exportInput.getInstance());
queryInput.setSession(exportInput.getSession());
queryInput.setTableName(exportInput.getTableName());
queryInput.setFilter(exportInput.getQueryFilter());
queryInput.setLimit(exportInput.getLimit());
/////////////////////////////////////////////////////////////////
// tell this query that it needs to put its output into a pipe //
@ -162,9 +163,9 @@ public class ReportAction
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// set up a report streamer, which will read rows from the pipe, and write formatted report rows to the output stream //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat = reportInput.getReportFormat();
ReportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
reportStreamer.start(reportInput, getFields(reportInput));
ReportFormat reportFormat = exportInput.getReportFormat();
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1");
//////////////////////////////////////////
// run the query action as an async job //
@ -207,8 +208,9 @@ public class ReportAction
lastReceivedRecordsAt = System.currentTimeMillis();
nextSleepMillis = INIT_SLEEP_MS;
int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe);
recordCount += recordsConsumed;
List<QRecord> records = recordPipe.consumeAvailableRecords();
reportStreamer.addRecords(records);
recordCount += records.size();
LOG.info(countFromPreExecute != null
? String.format("Processed %,d of %,d records so far", recordCount, countFromPreExecute)
@ -235,8 +237,9 @@ public class ReportAction
///////////////////////////////////////////////////
// send the final records to the report streamer //
///////////////////////////////////////////////////
int recordsConsumed = reportStreamer.takeRecordsFromPipe(recordPipe);
recordCount += recordsConsumed;
List<QRecord> records = recordPipe.consumeAvailableRecords();
reportStreamer.addRecords(records);
recordCount += records.size();
long reportEndTime = System.currentTimeMillis();
LOG.info((countFromPreExecute != null
@ -251,17 +254,17 @@ public class ReportAction
try
{
reportInput.getReportOutputStream().close();
exportInput.getReportOutputStream().close();
}
catch(Exception e)
{
throw (new QReportingException("Error completing report", e));
}
ReportOutput reportOutput = new ReportOutput();
reportOutput.setRecordCount(recordCount);
ExportOutput exportOutput = new ExportOutput();
exportOutput.setRecordCount(recordCount);
return (reportOutput);
return (exportOutput);
}
@ -269,12 +272,12 @@ public class ReportAction
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> getFields(ReportInput reportInput)
private List<QFieldMetaData> getFields(ExportInput exportInput)
{
QTableMetaData table = reportInput.getTable();
if(reportInput.getFieldNames() != null)
QTableMetaData table = exportInput.getTable();
if(exportInput.getFieldNames() != null)
{
return (reportInput.getFieldNames().stream().map(table::getField).toList());
return (exportInput.getFieldNames().stream().map(table::getField).toList());
}
else
{
@ -287,12 +290,12 @@ public class ReportAction
/*******************************************************************************
**
*******************************************************************************/
private void verifyCountUnderMax(ReportInput reportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException
private void verifyCountUnderMax(ExportInput exportInput, QBackendModuleInterface backendModule, ReportFormat reportFormat) throws QException
{
if(reportFormat.getMaxCols() != null)
{
List<QFieldMetaData> fields = getFields(reportInput);
if (fields.size() > reportFormat.getMaxCols())
List<QFieldMetaData> fields = getFields(exportInput);
if(fields.size() > reportFormat.getMaxCols())
{
throw (new QUserFacingException("The requested report would include more columns ("
+ String.format("%,d", fields.size()) + ") than the maximum allowed ("
@ -302,13 +305,13 @@ public class ReportAction
if(reportFormat.getMaxRows() != null)
{
if(reportInput.getLimit() == null || reportInput.getLimit() > reportFormat.getMaxRows())
if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows())
{
CountInterface countInterface = backendModule.getCountInterface();
CountInput countInput = new CountInput(reportInput.getInstance());
countInput.setSession(reportInput.getSession());
countInput.setTableName(reportInput.getTableName());
countInput.setFilter(reportInput.getQueryFilter());
CountInput countInput = new CountInput(exportInput.getInstance());
countInput.setSession(exportInput.getSession());
countInput.setTableName(exportInput.getTableName());
countInput.setFilter(exportInput.getQueryFilter());
CountOutput countOutput = countInterface.execute(countInput);
countFromPreExecute = countOutput.getCount();
if(countFromPreExecute > reportFormat.getMaxRows())

View File

@ -23,29 +23,46 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
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;
/*******************************************************************************
** Interface for various report formats to implement.
** Interface for various export formats to implement.
*******************************************************************************/
public interface ReportStreamerInterface
public interface ExportStreamerInterface
{
/*******************************************************************************
** Called once, before any rows are available. Meant to write a header, for example.
*******************************************************************************/
void start(ReportInput reportInput, List<QFieldMetaData> fields) throws QReportingException;
void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException;
/*******************************************************************************
** Called as records flow into the pipe.
******************************************************************************/
int takeRecordsFromPipe(RecordPipe recordPipe) throws QReportingException;
void addRecords(List<QRecord> recordList) throws QReportingException;
/*******************************************************************************
** Called once, after all rows are available. Meant to write a footer, or close resources, for example.
*******************************************************************************/
void finish() throws QReportingException;
/*******************************************************************************
**
*******************************************************************************/
default void setDisplayFormats(Map<String, String> displayFormats)
{
// noop in base class
}
/*******************************************************************************
**
*******************************************************************************/
default void addTotalsRow(QRecord record) throws QReportingException
{
addRecords(List.of(record));
}
}

View File

@ -0,0 +1,401 @@
/*
* 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.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Helper for Generating reports - to interpret formulas in report columns,
** that are in "excel-style", ala: =MINUS(47,42) or
** =IF(LT(ADD(${input.x},${input.y}),10,Yes,No)
*******************************************************************************/
public class FormulaInterpreter
{
/*******************************************************************************
** public method to interpret a formula. Takes a variableInterpreter, optionally
** full of maps of variables, and the formula string, assumed to have its leading
** '=' char already trimmed away.
*******************************************************************************/
public static Serializable interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula) throws QFormulaException
{
try
{
List<Serializable> results = interpretFormula(variableInterpreter, formula, new AtomicInteger(0));
if(results.size() == 1)
{
return (results.get(0));
}
else if(results.isEmpty())
{
throw (new QFormulaException("No results from formula"));
}
else
{
throw (new QFormulaException("More than 1 result from formula"));
}
}
catch(Exception e)
{
throw (new QFormulaException("Error interpreting formula [" + formula + "]", e));
}
}
/*******************************************************************************
** Recursive method that does the work of interpreting a formula.
** Uses AtomicInteger `i` to track index through the string into and out of
** recursive calls.
*******************************************************************************/
static List<Serializable> interpretFormula(QMetaDataVariableInterpreter variableInterpreter, String formula, AtomicInteger i) throws QFormulaException
{
StringBuilder token = new StringBuilder();
List<Serializable> result = new ArrayList<>();
char previousChar = 0;
while(i.get() < formula.length())
{
if(i.get() > 0)
{
previousChar = formula.charAt(i.get() - 1);
}
char c = formula.charAt(i.getAndIncrement());
if(c == '(' && i.get() < formula.length() - 1)
{
//////////////////////////////////////////////////////////////////////////////////////////
// open paren means: go into a sub-parse. Get back a list of arguments, and use those //
// as arguments for the current token, which must be a function name then. //
//////////////////////////////////////////////////////////////////////////////////////////
List<Serializable> args = interpretFormula(variableInterpreter, formula, i);
Serializable evaluate = evaluate(token.toString(), args, variableInterpreter);
result.add(evaluate);
}
else if(c == ')')
{
//////////////////////////////////////////////////////////////////////////
// close paren means: end this sub-parse. evaluate the current token, //
// add it to the result list, and return the result list. //
// unless we just closed a paren - then we can just return. //
//////////////////////////////////////////////////////////////////////////
if(previousChar != ')')
{
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate);
}
return (result);
}
else if(c == ',')
{
/////////////////////////////////////////////////////////////////////////
// comma means: evaluate the current token; add it to the result list //
// unless we just closed a paren - then we can just return. //
/////////////////////////////////////////////////////////////////////////
if(previousChar != ')')
{
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate);
}
token = new StringBuilder();
}
else
{
/////////////////////////////////////////////////
// else, we add this char to the current token //
/////////////////////////////////////////////////
token.append(c);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if we haven't found a result yet, assume we have just a literal, not a function call, and evaluate as such //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(result.isEmpty())
{
if(!token.isEmpty())
{
Serializable evaluate = evaluate(token.toString(), Collections.emptyList(), variableInterpreter);
result.add(evaluate);
}
}
return (result);
}
/*******************************************************************************
** Evaluate a token - maybe a literal, or variable, or function name -
** with arguments if it's a function, and in the context of the variableInterpreter.
*******************************************************************************/
private static Serializable evaluate(String token, List<Serializable> args, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
switch(token)
{
case "ADD":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).add(numbers.get(1)));
}
case "MINUS":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).subtract(numbers.get(1)));
}
case "MULTIPLY":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).multiply(numbers.get(1)));
}
case "DIVIDE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
{
return null;
}
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), 4, RoundingMode.HALF_UP));
}
case "DIVIDE_SCALE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 3, variableInterpreter);
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
{
return null;
}
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).divide(numbers.get(1), numbers.get(2).intValue(), RoundingMode.HALF_UP));
}
case "ROUND":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).round(new MathContext(numbers.get(1).intValue())));
}
case "SCALE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBigDecimal(numbers, () -> numbers.get(0).setScale(numbers.get(1).intValue(), RoundingMode.HALF_UP));
}
case "NVL":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return Objects.requireNonNullElse(numbers.get(0), numbers.get(1));
}
case "IF":
{
///////////////////////////////////////////////////////////////////////////////////////
// IF(CONDITION,TRUE,ELSE) //
// behavior in a spreadsheet appears to be: //
// booleans are evaluated naturally. //
// strings - if they look like 'true' or 'false, they are evaluated, else they error //
// numbers - 0 is false, all else are true. //
///////////////////////////////////////////////////////////////////////////////////////
List<Serializable> actualArgs = getArgumentList(args, 3, variableInterpreter);
Serializable condition = actualArgs.get(0);
boolean conditionBoolean;
if(condition == null)
{
conditionBoolean = false;
}
else if(condition instanceof Boolean b)
{
conditionBoolean = b;
}
else if(condition instanceof BigDecimal bd)
{
conditionBoolean = (bd.compareTo(BigDecimal.ZERO) != 0);
}
else if(condition instanceof String s)
{
if("true".equalsIgnoreCase(s))
{
conditionBoolean = true;
}
else if("false".equalsIgnoreCase(s))
{
conditionBoolean = false;
}
else
{
throw (new QFormulaException("Could not evaluate string '" + s + "' as a boolean."));
}
}
else
{
conditionBoolean = false;
}
return conditionBoolean ? actualArgs.get(1) : actualArgs.get(2);
}
case "LT":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) < 0);
}
case "LTE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) <= 0);
}
case "GT":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) > 0);
}
case "GTE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
return nullIfAnyNullArgsElseBoolean(numbers, () -> numbers.get(0).compareTo(numbers.get(1)) >= 0);
}
default:
{
////////////////////////////////////////////////////////////////////////////////////////
// if there aren't arguments, then we can try to evaluate the thing not as a function //
////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(args))
{
try
{
return (ValueUtils.getValueAsBigDecimal(token));
}
catch(Exception e)
{
// continue
}
try
{
return (variableInterpreter.interpret(token));
}
catch(Exception e)
{
// continue
}
}
}
}
throw (new QFormulaException("Unable to evaluate unrecognized expression: " + token + ""));
}
/*******************************************************************************
** if any number in the list is null, get back null - else, return the result of the supplier.
*******************************************************************************/
private static Serializable nullIfAnyNullArgsElseBigDecimal(List<BigDecimal> numbers, Supplier<BigDecimal> supplier)
{
if(numbers.stream().anyMatch(Objects::isNull))
{
return (null);
}
return supplier.get();
}
/*******************************************************************************
** if any number in the list is null, get back null - else, return the result of the supplier.
*******************************************************************************/
private static Serializable nullIfAnyNullArgsElseBoolean(List<BigDecimal> numbers, Supplier<Boolean> supplier)
{
if(numbers.stream().anyMatch(Objects::isNull))
{
return (null);
}
return supplier.get();
}
/*******************************************************************************
** given a list of arguments, get back a specific number of arguments, all of which we
** validate to be numbers (e.g., possibly interpreted variables) - else we throw.
** also throw if not the right number is present.
*******************************************************************************/
private static List<BigDecimal> getNumberArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
if(howMany != null)
{
if(!howMany.equals(originalArgs.size()))
{
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
}
}
List<BigDecimal> rs = new ArrayList<>();
for(Serializable originalArg : originalArgs)
{
try
{
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
rs.add(ValueUtils.getValueAsBigDecimal(interpretedArg));
}
catch(QValueException e)
{
throw (new QFormulaException("Could not process [" + originalArg + "] as a number"));
}
}
return (rs);
}
/*******************************************************************************
** given a list of arguments, get back a specific number of arguments, all of which we
** get interpreted. throw if not the right number of args is present.
*******************************************************************************/
private static List<Serializable> getArgumentList(List<Serializable> originalArgs, Integer howMany, QMetaDataVariableInterpreter variableInterpreter) throws QFormulaException
{
if(howMany != null)
{
if(!howMany.equals(originalArgs.size()))
{
throw (new QFormulaException("Wrong number of arguments (required: " + howMany + ", received: " + originalArgs.size() + ")"));
}
}
List<Serializable> rs = new ArrayList<>();
for(Serializable originalArg : originalArgs)
{
Serializable interpretedArg = variableInterpreter.interpretForObject(ValueUtils.getValueAsString(originalArg), null);
rs.add(interpretedArg);
}
return (rs);
}
}

View File

@ -0,0 +1,809 @@
/*
* 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.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.ReportViewCustomizer;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
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.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
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.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.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
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 com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
/*******************************************************************************
** Action to generate a report.
**
** A report can contain 1 or more Data Sources - e.g., tables + filters that define
** data that goes into the report, or simple data-supplier lambdas.
**
** A report can also contain 1 or more Views - e.g., sheets in a spreadsheet workbook.
** (how do those work in non-XLSX formats??). Views can either be:
** - plain tables,
** - summaries (like pivot tables, but called summary to avoid confusion with "native" pivot tables),
** - native pivot tables (not initially supported, due to lack of support in fastexcel...).
*******************************************************************************/
public class GenerateReportAction
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// summaryAggregates and varianceAggregates are multi-level maps, ala: //
// viewName > SummaryKey > fieldName > Aggregates //
// e.g.: //
// viewName: salesSummaryReport //
// SummaryKey: [(state:MO),(city:St.Louis)] //
// fieldName: salePrice //
// Aggregates: (count:47;sum:10,000;max:2,000;min:15) //
// salesSummaryReport > [(state:MO),(city:St.Louis)] > salePrice > (count:47;sum:10,000;max:2,000;min:15) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> summaryAggregates = new HashMap<>();
Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> varianceAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> totalAggregates = new HashMap<>();
Map<String, AggregatesInterface<?>> varianceTotalAggregates = new HashMap<>();
private QReportMetaData report;
private ReportFormat reportFormat;
private ExportStreamerInterface reportStreamer;
/*******************************************************************************
**
*******************************************************************************/
public void execute(ReportInput reportInput) throws QException
{
report = reportInput.getInstance().getReport(reportInput.getReportName());
reportFormat = reportInput.getReportFormat();
reportStreamer = reportFormat.newReportStreamer();
////////////////////////////////////////////////////////////////////////////////////////////////
// foreach data source, do a query (possibly more than 1, if it goes to multiple table views) //
////////////////////////////////////////////////////////////////////////////////////////////////
for(QReportDataSource dataSource : report.getDataSources())
{
//////////////////////////////////////////////////////////////////////////////
// make a list of the views that use this data source for various purposes. //
//////////////////////////////////////////////////////////////////////////////
List<QReportView> dataSourceTableViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.TABLE))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceSummaryViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getDataSourceName().equals(dataSource.getName()))
.toList();
List<QReportView> dataSourceVariantViews = report.getViews().stream()
.filter(v -> v.getType().equals(ReportType.SUMMARY))
.filter(v -> v.getVarianceDataSourceName() != null && v.getVarianceDataSourceName().equals(dataSource.getName()))
.toList();
/////////////////////////////////////////////////////////////////////////////////////////////
// if this data source isn't used for any table views, but it is used for one or //
// more summary views (possibly as a variant), then run the query, gathering summary data. //
/////////////////////////////////////////////////////////////////////////////////////////////
if(dataSourceTableViews.isEmpty())
{
if(!dataSourceSummaryViews.isEmpty() || !dataSourceVariantViews.isEmpty())
{
gatherData(reportInput, dataSource, null, dataSourceSummaryViews, dataSourceVariantViews);
}
}
else
{
////////////////////////////////////////////////////////////////////////////////////////
// else, foreach table view this data source is used for, run the data source's query //
////////////////////////////////////////////////////////////////////////////////////////
for(QReportView dataSourceTableView : dataSourceTableViews)
{
/////////////////////////////////////////////////////////////////////////////////////////
// if there's a view customizer, run it (e.g., to customize the columns in the report) //
/////////////////////////////////////////////////////////////////////////////////////////
if(dataSourceTableView.getViewCustomizer() != null)
{
Function<QReportView, QReportView> viewCustomizerFunction = QCodeLoader.getFunction(dataSourceTableView.getViewCustomizer());
if(viewCustomizerFunction instanceof ReportViewCustomizer reportViewCustomizer)
{
reportViewCustomizer.setReportInput(reportInput);
}
dataSourceTableView = viewCustomizerFunction.apply(dataSourceTableView.clone());
}
////////////////////////////////////////////////////////////////////////////////////
// start the table-view (e.g., open this tab in xlsx) and then run the query-loop //
////////////////////////////////////////////////////////////////////////////////////
startTableView(reportInput, dataSource, dataSourceTableView);
gatherData(reportInput, dataSource, dataSourceTableView, dataSourceSummaryViews, dataSourceVariantViews);
}
}
}
outputSummaries(reportInput);
}
/*******************************************************************************
**
*******************************************************************************/
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QReportingException
{
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession());
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
List<QFieldMetaData> fields;
if(CollectionUtils.nullSafeHasContents(reportView.getColumns()))
{
fields = new ArrayList<>();
for(QReportField column : reportView.getColumns())
{
if(column.getIsVirtual())
{
fields.add(column.toField());
}
else
{
QFieldMetaData field = table.getField(column.getName()).clone();
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());
}
/*******************************************************************************
**
*******************************************************************************/
private void gatherData(ReportInput reportInput, QReportDataSource dataSource, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////
// check if this view has a transform step - if so, set it up now and run its pre-run //
////////////////////////////////////////////////////////////////////////////////////////
AbstractTransformStep transformStep = null;
RunBackendStepInput transformStepInput = null;
RunBackendStepOutput transformStepOutput = null;
if(tableView != null && tableView.getRecordTransformStep() != null)
{
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStepInput = new RunBackendStepInput(reportInput.getInstance());
transformStepInput.setSession(reportInput.getSession());
transformStepInput.setValues(reportInput.getInputValues());
transformStepOutput = new RunBackendStepOutput();
transformStep.preRun(transformStepInput, transformStepOutput);
}
////////////////////////////////////////////////////////////////////
// create effectively-final versions of these vars for the lambda //
////////////////////////////////////////////////////////////////////
AbstractTransformStep finalTransformStep = transformStep;
RunBackendStepInput finalTransformStepInput = transformStepInput;
RunBackendStepOutput finalTransformStepOutput = transformStepOutput;
/////////////////////////////////////////////////////////////////
// run a record pipe loop, over the query for this data source //
/////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new RecordPipe();
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
{
if(dataSource.getSourceTable() != null)
{
QQueryFilter queryFilter = dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
QueryInput queryInput = new QueryInput(reportInput.getInstance());
queryInput.setSession(reportInput.getSession());
queryInput.setRecordPipe(recordPipe);
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this?
return (new QueryAction().execute(queryInput));
}
else if(dataSource.getStaticDataSupplier() != null)
{
@SuppressWarnings("unchecked")
Supplier<List<List<Serializable>>> supplier = QCodeLoader.getAdHoc(Supplier.class, dataSource.getStaticDataSupplier());
List<List<Serializable>> lists = supplier.get();
for(List<Serializable> list : lists)
{
QRecord record = new QRecord();
int index = 0;
for(Serializable value : list)
{
record.setValue("column" + (index++), value);
}
recordPipe.addRecord(record);
}
return (true);
}
else
{
throw (new IllegalStateException("Misconfigured data source [" + dataSource.getName() + "]."));
}
}, () ->
{
List<QRecord> records = recordPipe.consumeAvailableRecords();
if(finalTransformStep != null)
{
finalTransformStepInput.setRecords(records);
finalTransformStep.run(finalTransformStepInput, finalTransformStepOutput);
records = finalTransformStepOutput.getRecords();
}
return (consumeRecords(reportInput, dataSource, records, tableView, summaryViews, variantViews));
});
////////////////////////////////////////////////
// if there's a transformer, run its post-run //
////////////////////////////////////////////////
if(transformStep != null)
{
transformStep.postRun(transformStepInput, transformStepOutput);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void setInputValuesInQueryFilter(ReportInput reportInput, QQueryFilter queryFilter)
{
if(queryFilter == null || queryFilter.getCriteria() == null)
{
return;
}
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
for(QFilterCriteria criterion : queryFilter.getCriteria())
{
if(criterion.getValues() != null)
{
List<Serializable> newValues = new ArrayList<>();
for(Serializable value : criterion.getValues())
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpret(valueAsString);
newValues.add(interpretedValue);
}
criterion.setValues(newValues);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Integer consumeRecords(ReportInput reportInput, QReportDataSource dataSource, List<QRecord> records, QReportView tableView, List<QReportView> summaryViews, List<QReportView> variantViews) throws QException
{
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
////////////////////////////////////////////////////////////////////////////
// if this record goes on a table view, add it to the report streamer now //
////////////////////////////////////////////////////////////////////////////
if(tableView != null)
{
reportStreamer.addRecords(records);
}
/////////////////////////////////
// do aggregates for summaries //
/////////////////////////////////
if(summaryViews != null)
{
for(QReportView summaryView : summaryViews)
{
addRecordsToSummaryAggregates(summaryView, table, records, summaryAggregates);
}
}
if(variantViews != null)
{
for(QReportView variantView : variantViews)
{
addRecordsToSummaryAggregates(variantView, table, records, varianceAggregates);
}
}
///////////////////////////////////////////
// do totals too, if any views want them //
///////////////////////////////////////////
if(summaryViews != null && summaryViews.stream().anyMatch(QReportView::getIncludeTotalRow))
{
for(QRecord record : records)
{
addRecordToAggregatesMap(table, record, totalAggregates);
}
}
if(variantViews != null && variantViews.stream().anyMatch(QReportView::getIncludeTotalRow))
{
for(QRecord record : records)
{
addRecordToAggregatesMap(table, record, varianceTotalAggregates);
}
}
return (records.size());
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecordsToSummaryAggregates(QReportView view, QTableMetaData table, List<QRecord> records, Map<String, Map<SummaryKey, Map<String, AggregatesInterface<?>>>> aggregatesMap)
{
Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates = aggregatesMap.computeIfAbsent(view.getName(), (name) -> new HashMap<>());
for(QRecord record : records)
{
SummaryKey key = new SummaryKey();
for(String summaryField : view.getPivotFields())
{
Serializable summaryValue = record.getValue(summaryField);
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
summaryValue = record.getDisplayValue(summaryField);
}
key.add(summaryField, summaryValue);
if(view.getIncludePivotSubTotals() && key.getKeys().size() < view.getPivotFields().size())
{
/////////////////////////////////////////////////////////////////////////////////////////
// be careful here, with these key objects, and their identity, being used as map keys //
/////////////////////////////////////////////////////////////////////////////////////////
SummaryKey subKey = key.clone();
addRecordToSummaryKeyAggregates(table, record, viewAggregates, subKey);
}
}
addRecordToSummaryKeyAggregates(table, record, viewAggregates, key);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecordToSummaryKeyAggregates(QTableMetaData table, QRecord record, Map<SummaryKey, Map<String, AggregatesInterface<?>>> viewAggregates, SummaryKey key)
{
Map<String, AggregatesInterface<?>> keyAggregates = viewAggregates.computeIfAbsent(key, (name) -> new HashMap<>());
addRecordToAggregatesMap(table, record, keyAggregates);
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecordToAggregatesMap(QTableMetaData table, QRecord record, Map<String, AggregatesInterface<?>> aggregatesMap)
{
for(QFieldMetaData field : table.getFields().values())
{
if(field.getType().equals(QFieldType.INTEGER))
{
@SuppressWarnings("unchecked")
AggregatesInterface<Integer> fieldAggregates = (AggregatesInterface<Integer>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new IntegerAggregates());
fieldAggregates.add(record.getValueInteger(field.getName()));
}
else if(field.getType().equals(QFieldType.DECIMAL))
{
@SuppressWarnings("unchecked")
AggregatesInterface<BigDecimal> fieldAggregates = (AggregatesInterface<BigDecimal>) aggregatesMap.computeIfAbsent(field.getName(), (name) -> new BigDecimalAggregates());
fieldAggregates.add(record.getValueBigDecimal(field.getName()));
}
// todo - more types (dates, at least?)
}
}
/*******************************************************************************
**
*******************************************************************************/
private void outputSummaries(ReportInput reportInput) throws QReportingException, QFormulaException
{
List<QReportView> reportViews = report.getViews().stream().filter(v -> v.getType().equals(ReportType.SUMMARY)).toList();
for(QReportView view : reportViews)
{
QReportDataSource dataSource = report.getDataSource(view.getDataSourceName());
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession());
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(summaryOutput.titleRow);
exportInput.setIncludeHeaderRow(view.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
reportStreamer.setDisplayFormats(getDisplayFormatMap(view));
reportStreamer.start(exportInput, getFields(table, view), view.getLabel());
reportStreamer.addRecords(summaryOutput.summaryRows); // todo - what if this set is huge?
if(summaryOutput.totalRow != null)
{
reportStreamer.addTotalsRow(summaryOutput.totalRow);
}
}
reportStreamer.finish();
}
/*******************************************************************************
**
*******************************************************************************/
private Map<String, String> getDisplayFormatMap(QReportView view)
{
return (view.getColumns().stream()
.filter(c -> c.getDisplayFormat() != null)
.collect(Collectors.toMap(QReportField::getName, QReportField::getDisplayFormat)));
}
/*******************************************************************************
**
*******************************************************************************/
private Map<String, String> getDisplayFormatMap(List<QFieldMetaData> fields)
{
return (fields.stream()
.filter(f -> f.getDisplayFormat() != null)
.collect(Collectors.toMap(QFieldMetaData::getName, QFieldMetaData::getDisplayFormat)));
}
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> getFields(QTableMetaData table, QReportView view)
{
List<QFieldMetaData> fields = new ArrayList<>();
for(String pivotField : view.getPivotFields())
{
QFieldMetaData field = table.getField(pivotField);
fields.add(new QFieldMetaData(pivotField, field.getType()).withLabel(field.getLabel())); // todo do we need the type? if so need table as input here
}
for(QReportField column : view.getColumns())
{
fields.add(new QFieldMetaData().withName(column.getName()).withLabel(column.getLabel())); // todo do we need the type? if so need table as input here
}
return (fields);
}
/*******************************************************************************
**
*******************************************************************************/
private SummaryOutput computeSummaryRowsForView(ReportInput reportInput, QReportView view, QTableMetaData table) throws QReportingException, QFormulaException
{
QValueFormatter valueFormatter = new QValueFormatter();
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
variableInterpreter.addValueMap("total", getSummaryValuesForInterpreter(totalAggregates));
///////////
// title //
///////////
String title = getTitle(view, variableInterpreter);
/////////////////////////
// create summary rows //
/////////////////////////
List<QRecord> summaryRows = new ArrayList<>();
for(Map.Entry<SummaryKey, Map<String, AggregatesInterface<?>>> entry : summaryAggregates.getOrDefault(view.getName(), Collections.emptyMap()).entrySet())
{
SummaryKey summaryKey = entry.getKey();
Map<String, AggregatesInterface<?>> fieldAggregates = entry.getValue();
Map<String, Serializable> summaryValues = getSummaryValuesForInterpreter(fieldAggregates);
variableInterpreter.addValueMap("pivot", summaryValues);
variableInterpreter.addValueMap("summary", summaryValues);
HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues);
if(!varianceAggregates.isEmpty())
{
Map<SummaryKey, Map<String, AggregatesInterface<?>>> varianceMap = varianceAggregates.getOrDefault(view.getName(), Collections.emptyMap());
Map<String, AggregatesInterface<?>> varianceSubMap = varianceMap.getOrDefault(summaryKey, Collections.emptyMap());
Map<String, Serializable> varianceValues = getSummaryValuesForInterpreter(varianceSubMap);
variableInterpreter.addValueMap("variancePivot", varianceValues);
variableInterpreter.addValueMap("variance", varianceValues);
}
QRecord summaryRow = new QRecord();
summaryRows.add(summaryRow);
////////////////////////////
// add the summary values //
////////////////////////////
for(Pair<String, Serializable> key : summaryKey.getKeys())
{
summaryRow.setValue(key.getA(), key.getB());
}
///////////////////////////////////////////////////////////////////////////////
// for summary subtotals, add the text "Total" to the last field in this key //
///////////////////////////////////////////////////////////////////////////////
if(summaryKey.getKeys().size() < view.getPivotFields().size())
{
String fieldName = summaryKey.getKeys().get(summaryKey.getKeys().size() - 1).getA();
summaryRow.setValue(fieldName, summaryRow.getValueString(fieldName) + " Total");
}
///////////////////////////
// add the column values //
///////////////////////////
for(QReportField column : view.getColumns())
{
Serializable serializable = getValueForColumn(variableInterpreter, column);
summaryRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
}
}
//////////////////////////////////////////////////////////////////////////////////////
// sort the summary rows //
// Note - this will NOT work correctly if there's more than 1 pivot field, as we're //
// not doing anything to keep related rows them together (e.g., all MO state rows) //
//////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(view.getOrderByFields()))
{
summaryRows.sort((o1, o2) ->
{
return summaryRowComparator(view, o1, o2);
});
}
////////////////
// totals row //
////////////////
QRecord totalRow = null;
if(view.getIncludeTotalRow())
{
totalRow = new QRecord();
for(String pivotField : view.getPivotFields())
{
if(totalRow.getValues().isEmpty())
{
totalRow.setValue(pivotField, "Totals");
}
}
Map<String, Serializable> totalValues = getSummaryValuesForInterpreter(totalAggregates);
variableInterpreter.addValueMap("pivot", totalValues);
variableInterpreter.addValueMap("summary", totalValues);
Map<String, Serializable> varianceTotalValues = getSummaryValuesForInterpreter(varianceTotalAggregates);
variableInterpreter.addValueMap("variancePivot", varianceTotalValues);
variableInterpreter.addValueMap("variance", varianceTotalValues);
HashMap<String, Serializable> thisRowValues = new HashMap<>();
variableInterpreter.addValueMap("thisRow", thisRowValues);
for(QReportField column : view.getColumns())
{
Serializable serializable = getValueForColumn(variableInterpreter, column);
totalRow.setValue(column.getName(), serializable);
thisRowValues.put(column.getName(), serializable);
String formatted = valueFormatter.formatValue(column.getDisplayFormat(), serializable);
}
}
return (new SummaryOutput(summaryRows, title, totalRow));
}
/*******************************************************************************
**
*******************************************************************************/
private String getTitle(QReportView view, QMetaDataVariableInterpreter variableInterpreter)
{
String title = null;
if(view.getTitleFields() != null && StringUtils.hasContent(view.getTitleFormat()))
{
List<String> titleValues = new ArrayList<>();
for(String titleField : view.getTitleFields())
{
titleValues.add(variableInterpreter.interpret(titleField));
}
title = new QValueFormatter().formatStringWithValues(view.getTitleFormat(), titleValues);
}
else if(StringUtils.hasContent(view.getTitleFormat()))
{
title = view.getTitleFormat();
}
return title;
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable getValueForColumn(QMetaDataVariableInterpreter variableInterpreter, QReportField column) throws QFormulaException
{
String formula = column.getFormula();
Serializable result;
if(formula.startsWith("=") && formula.length() > 1)
{
result = FormulaInterpreter.interpretFormula(variableInterpreter, formula.substring(1));
}
else
{
result = variableInterpreter.interpretForObject(formula, null);
}
return (result);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings({ "rawtypes", "unchecked" })
private int summaryRowComparator(QReportView view, QRecord o1, QRecord o2)
{
if(o1 == o2)
{
return (0);
}
for(QFilterOrderBy orderByField : view.getOrderByFields())
{
Comparable c1 = (Comparable) o1.getValue(orderByField.getFieldName());
Comparable c2 = (Comparable) o2.getValue(orderByField.getFieldName());
if(c1 == null && c2 == null)
{
continue;
}
if(c1 == null)
{
return (orderByField.getIsAscending() ? -1 : 1);
}
if(c2 == null)
{
return (orderByField.getIsAscending() ? 1 : -1);
}
int comp = orderByField.getIsAscending() ? c1.compareTo(c2) : c2.compareTo(c1);
if(comp != 0)
{
return (comp);
}
}
return (0);
}
/*******************************************************************************
**
*******************************************************************************/
private Map<String, Serializable> getSummaryValuesForInterpreter(Map<String, AggregatesInterface<?>> fieldAggregates)
{
Map<String, Serializable> summaryValuesForInterpreter = new HashMap<>();
for(Map.Entry<String, AggregatesInterface<?>> subEntry : fieldAggregates.entrySet())
{
String fieldName = subEntry.getKey();
AggregatesInterface<?> aggregates = subEntry.getValue();
summaryValuesForInterpreter.put("sum." + fieldName, aggregates.getSum());
summaryValuesForInterpreter.put("count." + fieldName, aggregates.getCount());
summaryValuesForInterpreter.put("min." + fieldName, aggregates.getMin());
summaryValuesForInterpreter.put("max." + fieldName, aggregates.getMax());
summaryValuesForInterpreter.put("average." + fieldName, aggregates.getAverage());
}
return summaryValuesForInterpreter;
}
/*******************************************************************************
** record to serve as tuple/multi-value output of computeSummaryRowsForView method.
*******************************************************************************/
private record SummaryOutput(List<QRecord> summaryRows, String titleRow, QRecord totalRow)
{
}
}

View File

@ -0,0 +1,162 @@
/*
* 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.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
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;
/*******************************************************************************
** Report streamer implementation that just builds up a STATIC list of lists of strings.
** Meant only for use in unit tests at this time... would need refactored for
** multi-thread/multi-use if wanted for real usage.
*******************************************************************************/
public class ListOfMapsExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ListOfMapsExportStreamer.class);
private ExportInput exportInput;
private List<QFieldMetaData> fields;
private static Map<String, List<Map<String, String>>> rows = new LinkedHashMap<>();
private static Map<String, List<String>> headers = new LinkedHashMap<>();
private static String currentSheetLabel;
/*******************************************************************************
**
*******************************************************************************/
public ListOfMapsExportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
public static void reset()
{
rows.clear();
headers.clear();
currentSheetLabel = null;
}
/*******************************************************************************
** Getter for list
**
*******************************************************************************/
public static List<Map<String, String>> getList(String name)
{
return (rows.get(name));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
currentSheetLabel = label;
rows.put(label, new ArrayList<>());
if(exportInput.getIncludeHeaderRow())
{
headers.put(label, new ArrayList<>());
for(QFieldMetaData field : fields)
{
headers.get(label).add(field.getLabel());
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords)
{
addRecord(qRecord);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void addRecord(QRecord qRecord)
{
Map<String, String> row = new LinkedHashMap<>();
rows.get(currentSheetLabel).add(row);
for(int i = 0; i < fields.size(); i++)
{
row.put(headers.get(currentSheetLabel).get(i), qRecord.getValueString(fields.get(i).getName()));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addTotalsRow(QRecord record)
{
addRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish()
{
}
}

View File

@ -41,8 +41,13 @@ public class RecordPipe
{
private static final Logger LOG = LogManager.getLogger(RecordPipe.class);
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
private ArrayBlockingQueue<QRecord> queue = new ArrayBlockingQueue<>(1_000);
private boolean isTerminated = false;
private Consumer<List<QRecord>> postRecordActions = null;
/////////////////////////////////////
@ -51,11 +56,31 @@ public class RecordPipe
private List<QRecord> singleRecordListForPostRecordActions = new ArrayList<>();
/*******************************************************************************
** Add a record to the pipe. Will block if the pipe is full.
** Turn off the pipe. Stop accepting new records (just ignore them in the add
** method). Clear the existing queue. Don't return any more records. Note that
** if consumeAvailableRecords was running in another thread, it may still return
** some records that it read before this call.
*******************************************************************************/
public void terminate()
{
isTerminated = true;
queue.clear();
}
/*******************************************************************************
** Add a record to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecord(QRecord record)
{
if(isTerminated)
{
return;
}
if(postRecordActions != null)
{
////////////////////////////////////////////////////////////////////////////////////
@ -82,18 +107,29 @@ public class RecordPipe
{
boolean offerResult = queue.offer(record);
while(!offerResult)
if(!offerResult && !isTerminated)
{
LOG.debug("Record pipe.add failed (due to full pipe). Blocking.");
SleepUtils.sleep(100, TimeUnit.MILLISECONDS);
long sleepLoopStartTime = System.currentTimeMillis();
long now = System.currentTimeMillis();
while(!offerResult && !isTerminated)
{
if(now - sleepLoopStartTime > MAX_SLEEP_LOOP_MILLIS)
{
LOG.warn("Giving up adding record to pipe, due to pipe being full for more than {} millis", MAX_SLEEP_LOOP_MILLIS);
throw (new IllegalStateException("Giving up adding record to pipe, due to pipe staying full too long."));
}
LOG.trace("Record pipe.add failed (due to full pipe). Blocking.");
SleepUtils.sleep(BLOCKING_SLEEP_MILLIS, TimeUnit.MILLISECONDS);
offerResult = queue.offer(record);
now = System.currentTimeMillis();
}
}
}
/*******************************************************************************
** Add a list of records to the pipe
** Add a list of records to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecords(List<QRecord> records)
{
@ -117,7 +153,7 @@ public class RecordPipe
{
List<QRecord> rs = new ArrayList<>();
while(true)
while(!isTerminated)
{
QRecord record = queue.poll();
if(record == null)
@ -137,6 +173,11 @@ public class RecordPipe
*******************************************************************************/
public int countAvailableRecords()
{
if(isTerminated)
{
return (0);
}
return (queue.size());
}

View File

@ -0,0 +1,131 @@
/*
* 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.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.utils.Pair;
/*******************************************************************************
** For a summary report, a list of field/value pairs that make up a "key".
**
** For example, in a report doing summaries by State > City > ZipCode, a SummaryKey
** would look like: [(state:MO),(city:St.Louis),(zipCode:63101)].
*******************************************************************************/
public class SummaryKey implements Cloneable
{
private List<Pair<String, Serializable>> keys = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public SummaryKey()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "PivotKey{keys=" + keys + '}';
}
/*******************************************************************************
**
*******************************************************************************/
public void add(String field, Serializable value)
{
keys.add(new Pair<>(field, value));
}
/*******************************************************************************
** Getter for keys
**
*******************************************************************************/
public List<Pair<String, Serializable>> getKeys()
{
return keys;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
SummaryKey summaryKey = (SummaryKey) o;
return Objects.equals(keys, summaryKey.keys);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int hashCode()
{
return Objects.hash(keys);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public SummaryKey clone()
{
SummaryKey clone = new SummaryKey();
for(Pair<String, Serializable> key : keys)
{
clone.add(key.getA(), key.getB());
}
return (clone);
}
}

View File

@ -0,0 +1,42 @@
/*
* 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 java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
/*******************************************************************************
** Interface for customizer on a QReportView. Extends Function by adding setter
** method for reportInput.
*******************************************************************************/
public interface ReportViewCustomizer extends Function<QReportView, QReportView>
{
/*******************************************************************************
**
*******************************************************************************/
void setReportInput(ReportInput reportInput);
}

View File

@ -0,0 +1,71 @@
/*
* 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.excelformatting;
import org.dhatim.fastexcel.BorderSide;
import org.dhatim.fastexcel.BorderStyle;
import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Version of excel styler that does bold headers and footers, with basic borders.
*******************************************************************************/
public class BoldHeaderAndFooterExcelStyler implements ExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleTitleRow(StyleSetter titleRowStyle)
{
titleRowStyle
.bold()
.fontSize(14)
.horizontalAlignment("center");
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void styleHeaderRow(StyleSetter headerRowStyle)
{
headerRowStyle
.bold()
.borderStyle(BorderSide.BOTTOM, BorderStyle.THIN);
}
@Override
public void styleTotalsRow(StyleSetter totalsRowStyle)
{
totalsRowStyle
.bold()
.borderStyle(BorderSide.TOP, BorderStyle.THIN)
.borderStyle(BorderSide.BOTTOM, BorderStyle.DOUBLE);
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.excelformatting;
import org.dhatim.fastexcel.StyleSetter;
/*******************************************************************************
** Interface for classes that know how to apply styles to an Excel stream being
** built by fastexcel.
*******************************************************************************/
public interface ExcelStylerInterface
{
/*******************************************************************************
**
*******************************************************************************/
default void styleTitleRow(StyleSetter titleRowStyle)
{
}
/*******************************************************************************
**
*******************************************************************************/
default void styleHeaderRow(StyleSetter headerRowStyle)
{
}
/*******************************************************************************
**
*******************************************************************************/
default void styleTotalsRow(StyleSetter totalsRowStyle)
{
}
}

View File

@ -0,0 +1,31 @@
/*
* 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.excelformatting;
/*******************************************************************************
** Excel styler that does nothing - just takes defaults (which are all no-op) from the interface.
*******************************************************************************/
public class PlainExcelStyler implements ExcelStylerInterface
{
}

View File

@ -0,0 +1,119 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Action to execute user/runtime defined code.
**
** This action is designed to support code in multiple languages, by using
** executors, e.g., provided by additional runtime qqq dependencies. Initially
** we are building qqq-language-support-javascript.
**
** We also have a Java executor, to provide at least a little bit of testability
** within qqq-backend-core. This executor is a candidate to be replaced in the
** future with something that would do actual dynamic java (whether that's compiled
** at runtime, or loaded from a plugin jar at runtime). In other words, the java
** executor in place today is just meant to be a placeholder.
*******************************************************************************/
public class ExecuteCodeAction
{
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public void run(ExecuteCodeInput input, ExecuteCodeOutput output) throws QException, QCodeException
{
QCodeReference codeReference = input.getCodeReference();
QCodeExecutionLoggerInterface executionLogger = input.getExecutionLogger();
if(executionLogger == null)
{
executionLogger = getDefaultExecutionLogger();
}
executionLogger.acceptExecutionStart(input);
try
{
String languageExecutor = switch(codeReference.getCodeType())
{
case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
};
@SuppressWarnings("unchecked")
Class<? extends QCodeExecutor> executorClass = (Class<? extends QCodeExecutor>) Class.forName(languageExecutor);
QCodeExecutor qCodeExecutor = executorClass.getConstructor().newInstance();
////////////////////////////////////////////////////////////////////////////////////////////////////
// merge all of the input context, plus the input... input - into a context for the code executor //
////////////////////////////////////////////////////////////////////////////////////////////////////
Map<String, Serializable> context = new HashMap<>();
if(input.getContext() != null)
{
context.putAll(input.getContext());
}
if(input.getInput() != null)
{
context.putAll(input.getInput());
}
Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger);
output.setOutput(codeOutput);
executionLogger.acceptExecutionEnd(codeOutput);
}
catch(QCodeException qCodeException)
{
executionLogger.acceptException(qCodeException);
throw (qCodeException);
}
catch(Exception e)
{
executionLogger.acceptException(e);
throw (new QException("Error executing code [" + codeReference + "]", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private QCodeExecutionLoggerInterface getDefaultExecutionLogger()
{
return (new Log4jCodeExecutionLogger());
}
}

View File

@ -0,0 +1,44 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Interface to be implemented by language-specific code executors, e.g., in
** qqq-language-support-${languageName} maven modules.
*******************************************************************************/
public interface QCodeExecutor
{
/*******************************************************************************
**
*******************************************************************************/
Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
}

View File

@ -0,0 +1,68 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
** Java
*******************************************************************************/
public class QJavaExecutor implements QCodeExecutor
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException
{
Map<String, Object> context = new HashMap<>(inputContext);
if(!context.containsKey("logger"))
{
context.put("logger", executionLogger);
}
Serializable output;
try
{
Function<Map<String, Object>, Serializable> function = QCodeLoader.getFunction(codeReference);
output = function.apply(context);
}
catch(Exception e)
{
QCodeException qCodeException = new QCodeException("Error executing script", e);
throw (qCodeException);
}
return (output);
}
}

View File

@ -0,0 +1,152 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
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;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptInput;
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.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
/*******************************************************************************
**
*******************************************************************************/
public class RunAssociatedScriptAction
{
/*******************************************************************************
**
*******************************************************************************/
public void run(RunAssociatedScriptInput input, RunAssociatedScriptOutput output) throws QException
{
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() + "]"));
}
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.setInput(new HashMap<>(input.getInputValues()));
executeCodeInput.setContext(new HashMap<>());
if(input.getOutputObject() != null)
{
executeCodeInput.getContext().put("output", input.getOutputObject());
}
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()));
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutput(executeCodeOutput.getOutput());
}
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
getInput.setTableName("scriptRevision");
getInput.setPrimaryKey(scriptRevisionId);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("The current revision of the script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ input.getCodeReference().getFieldName() + "] (scriptRevisionId=" + scriptRevisionId + ") was not found."));
}
return (new ScriptRevision(getOutput.getRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
getInput.setTableName("script");
getInput.setPrimaryKey(scriptId);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "]["
+ input.getCodeReference().getFieldName() + "] (script id=" + scriptId + ") was not found."));
}
return (new Script(getOutput.getRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
private Serializable getScriptId(RunAssociatedScriptInput input) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
getInput.setTableName(input.getCodeReference().getRecordTable());
getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey());
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
throw (new QNotFoundException("The requested record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey() + "] was not found."));
}
return (getOutput.getRecord().getValue(input.getCodeReference().getFieldName()));
}
}

View File

@ -0,0 +1,222 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.List;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
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.QFilterOrderBy;
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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Action to store a new version of a script, associated with a record.
**
** If there's never been a script assigned to the record (for the specified field),
** then a new Script record is first inserted.
**
** The script referenced by the record is always updated to point at the new
** scriptRevision record that is inserted.
**
*******************************************************************************/
public class StoreAssociatedScriptAction
{
/*******************************************************************************
**
*******************************************************************************/
public void run(StoreAssociatedScriptInput input, StoreAssociatedScriptOutput output) throws QException
{
ActionHelper.validateSession(input);
QTableMetaData table = input.getTable();
Optional<AssociatedScript> optAssociatedScript = table.getAssociatedScripts().stream().filter(as -> as.getFieldName().equals(input.getFieldName())).findFirst();
if(optAssociatedScript.isEmpty())
{
throw (new QException("Field to update associated script for is not an associated script field."));
}
AssociatedScript associatedScript = optAssociatedScript.get();
/////////////////////////////////////////////////////////////
// get the record that the script is to be associated with //
/////////////////////////////////////////////////////////////
QRecord associatedRecord;
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
getInput.setTableName(input.getTableName());
getInput.setPrimaryKey(input.getRecordPrimaryKey());
getInput.setShouldGenerateDisplayValues(true);
GetOutput getOutput = new GetAction().execute(getInput);
associatedRecord = getOutput.getRecord();
}
if(associatedRecord == null)
{
throw (new QException("Record to associated with script was not found."));
}
//////////////////////////////////////////////////////////////////
// check if there's currently a script referenced by the record //
//////////////////////////////////////////////////////////////////
Serializable existingScriptId = associatedRecord.getValueString(input.getFieldName());
QRecord script;
Integer nextSequenceNo = 1;
if(existingScriptId == null)
{
////////////////////////////////////////////////////////////////////
// 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.setTableName("scriptType");
getInput.setPrimaryKey(associatedScript.getScriptTypeId());
getInput.setShouldGenerateDisplayValues(true);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord scriptType = getOutput.getRecord();
if(scriptType == null)
{
throw (new QException("Script type [" + associatedScript.getScriptTypeId() + "] was not found."));
}
/////////////////////////
// insert a new script //
/////////////////////////
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.setTableName("script");
insertInput.setRecords(List.of(script));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
script = insertOutput.getRecords().get(0);
/////////////////////////////////////////////////////////////
// update the associated record to point at the new script //
/////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput(input.getInstance());
updateInput.setSession(input.getSession());
updateInput.setTableName(input.getTableName());
updateInput.setRecords(List.of(new QRecord()
.withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField()))
.withValue(input.getFieldName(), script.getValue("id"))
));
new UpdateAction().execute(updateInput);
}
else
{
////////////////////////////////////////
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
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.setTableName("scriptRevision");
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
);
queryInput.setLimit(1);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(!queryOutput.getRecords().isEmpty())
{
nextSequenceNo = queryOutput.getRecords().get(0).getValueInteger("sequenceNo") + 1;
}
}
//////////////////////////////////
// insert a new script revision //
//////////////////////////////////
String commitMessage = input.getCommitMessage();
if(!StringUtils.hasContent(commitMessage))
{
if(nextSequenceNo == 1)
{
commitMessage = "Initial version";
}
else
{
commitMessage = "No commit message given";
}
}
QRecord scriptRevision = new QRecord()
.withValue("scriptId", script.getValue("id"))
.withValue("contents", input.getCode())
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);
try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
}
InsertInput insertInput = new InsertInput(input.getInstance());
insertInput.setSession(input.getSession());
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput(input.getInstance());
updateInput.setSession(input.getSession());
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);
}
}

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.scripts;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Class for running a test of a script - e.g., maybe before it is saved.
*******************************************************************************/
public class TestScriptAction
{
/*******************************************************************************
**
*******************************************************************************/
public void run(TestScriptInput input, TestScriptOutput output) throws QException
{
QTableMetaData table = input.getTable();
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
executeCodeInput.setSession(input.getSession());
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
executeCodeInput.setContext(new HashMap<>());
// todo! if(input.getOutputObject() != null)
// todo! {
// todo! executeCodeInput.getContext().put("output", input.getOutputObject());
// todo! }
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(input.getCode()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
executeCodeInput.setExecutionLogger(new BuildScriptLogAndScriptLogLineExecutionLogger());
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
// todo! output.setOutput(executeCodeOutput.getOutput());
}
}

View File

@ -0,0 +1,221 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
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;
/*******************************************************************************
** Implementation of a code execution logger that builds a scriptLog and 0 or more
** 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
{
private static final Logger LOG = LogManager.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
private QRecord scriptLog;
private List<QRecord> scriptLogLines = new ArrayList<>();
protected ExecuteCodeInput executeCodeInput;
private Serializable scriptId;
private Serializable scriptRevisionId;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BuildScriptLogAndScriptLogLineExecutionLogger()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public BuildScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
{
this.scriptId = scriptId;
this.scriptRevisionId = scriptRevisionId;
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord buildHeaderRecord(ExecuteCodeInput executeCodeInput)
{
return (new QRecord()
.withValue("scriptId", scriptId)
.withValue("scriptRevisionId", scriptRevisionId)
.withValue("startTimestamp", Instant.now())
.withValue("input", truncate(executeCodeInput.getInput())));
}
/*******************************************************************************
**
*******************************************************************************/
protected QRecord buildDetailLogRecord(String logLine)
{
return (new QRecord()
.withValue("scriptLogId", scriptLog.getValue("id"))
.withValue("timestamp", Instant.now())
.withValue("text", truncate(logLine)));
}
/*******************************************************************************
**
*******************************************************************************/
private String truncate(Object o)
{
return StringUtils.safeTruncate(ValueUtils.getValueAsString(o), 1000, "...");
}
/*******************************************************************************
**
*******************************************************************************/
protected void updateHeaderAtEnd(Serializable output, Exception exception)
{
Instant startTimestamp = (Instant) scriptLog.getValue("startTimestamp");
Instant endTimestamp = Instant.now();
scriptLog.setValue("endTimestamp", endTimestamp);
scriptLog.setValue("runTimeMillis", startTimestamp.until(endTimestamp, ChronoUnit.MILLIS));
if(exception != null)
{
scriptLog.setValue("hadError", true);
scriptLog.setValue("error", exception.getMessage());
}
else
{
scriptLog.setValue("hadError", false);
scriptLog.setValue("output", truncate(output));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
try
{
this.executeCodeInput = executeCodeInput;
this.scriptLog = buildHeaderRecord(executeCodeInput);
}
catch(Exception e)
{
LOG.warn("Error starting storage of script log", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptLogLine(String logLine)
{
scriptLogLines.add(buildDetailLogRecord(logLine));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
updateHeaderAtEnd(null, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
updateHeaderAtEnd(output, null);
}
/*******************************************************************************
** Getter for scriptLog
**
*******************************************************************************/
public QRecord getScriptLog()
{
return scriptLog;
}
/*******************************************************************************
** Getter for scriptLogLines
**
*******************************************************************************/
public List<QRecord> getScriptLogLines()
{
return scriptLogLines;
}
/*******************************************************************************
** Setter for scriptLog
**
*******************************************************************************/
protected void setScriptLog(QRecord scriptLog)
{
this.scriptLog = scriptLog;
}
}

View File

@ -0,0 +1,93 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import java.util.UUID;
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;
/*******************************************************************************
** Implementation of a code execution logger that logs to LOG 4j
*******************************************************************************/
public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
private static final Logger LOG = LogManager.getLogger(Log4jCodeExecutionLogger.class);
private QCodeReference qCodeReference;
private String uuid = UUID.randomUUID().toString();
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
this.qCodeReference = executeCodeInput.getCodeReference();
String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptLogLine(String logLine)
{
LOG.info("Script log: " + uuid + ": " + logLine);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
LOG.info("Script Exception: " + uuid, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "...");
LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString);
}
}

View File

@ -0,0 +1,74 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
/*******************************************************************************
** Implementation of a code execution logger that just noop's every action.
*******************************************************************************/
public class NoopCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptLogLine(String logLine)
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
}
}

View File

@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
/*******************************************************************************
** Interface to provide logging functionality to QCodeExecution (e.g., scripts)
*******************************************************************************/
public interface QCodeExecutionLoggerInterface
{
/*******************************************************************************
** Called when the execution starts - takes the execution's input object.
*******************************************************************************/
void acceptExecutionStart(ExecuteCodeInput executeCodeInput);
/*******************************************************************************
** Called to log a line, a message.
*******************************************************************************/
void acceptLogLine(String logLine);
/*******************************************************************************
** In case the loggerInterface object is provided to the script as context,
** this method gives a clean interface for the script to log a line.
*******************************************************************************/
default void log(String message)
{
acceptLogLine(message);
}
/*******************************************************************************
** Called if the script fails with an exception.
*******************************************************************************/
void acceptException(Exception exception);
/*******************************************************************************
** Called if the script completes without exception.
*******************************************************************************/
void acceptExecutionEnd(Serializable output);
}

View File

@ -0,0 +1,136 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import 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.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;
/*******************************************************************************
** Implementation of a code execution logger that logs into scriptLog and scriptLogLine
** tables - e.g., as defined in ScriptMetaDataProvider.
*******************************************************************************/
public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger
{
private static final Logger LOG = LogManager.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public StoreScriptLogAndScriptLogLineExecutionLogger(Serializable scriptId, Serializable scriptRevisionId)
{
super(scriptId, scriptRevisionId);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
try
{
super.acceptExecutionStart(executeCodeInput);
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
insertInput.setSession(executeCodeInput.getSession());
insertInput.setTableName("scriptLog");
insertInput.setRecords(List.of(getScriptLog()));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
setScriptLog(insertOutput.getRecords().get(0));
}
catch(Exception e)
{
LOG.warn("Error starting storage of script log", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
store(null, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
store(output, null);
}
/*******************************************************************************
**
*******************************************************************************/
private void store(Serializable output, Exception exception)
{
try
{
updateHeaderAtEnd(output, exception);
UpdateInput updateInput = new UpdateInput(executeCodeInput.getInstance());
updateInput.setSession(executeCodeInput.getSession());
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.setTableName("scriptLogLine");
insertInput.setRecords(getScriptLogLines());
new InsertAction().execute(insertInput);
}
}
catch(Exception e)
{
LOG.warn("Error storing script log", e);
}
}
}

View File

@ -0,0 +1,161 @@
/*
* 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;
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.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.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.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.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
/*******************************************************************************
** Action to run a get against a table.
**
*******************************************************************************/
public class GetAction
{
private Optional<Function<QRecord, QRecord>> postGetRecordCustomizer;
private GetInput getInput;
private QValueFormatter qValueFormatter;
private QPossibleValueTranslator qPossibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
public GetOutput execute(GetInput getInput) throws QException
{
ActionHelper.validateSession(getInput);
postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
this.getInput = getInput;
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(getInput.getBackend());
// todo pre-customization - just get to modify the request?
GetInterface getInterface = null;
try
{
getInterface = qModule.getGetInterface();
}
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. //
////////////////////////////////////////////////////////////////////////////////////////////////
}
GetOutput getOutput;
if(getInterface != null)
{
getOutput = getInterface.execute(getInput);
}
else
{
getOutput = performGetViaQuery(getInput);
}
if(getOutput.getRecord() != null)
{
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
}
return getOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private GetOutput performGetViaQuery(GetInput getInput) throws QException
{
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())
{
getOutput.setRecord(queryOutput.getRecords().get(0));
}
return (getOutput);
}
/*******************************************************************************
** Run the necessary actions on a record. This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public QRecord postRecordActions(QRecord record)
{
QRecord returnRecord = record;
if(this.postGetRecordCustomizer.isPresent())
{
returnRecord = postGetRecordCustomizer.get().apply(record);
}
if(getInput.getShouldTranslatePossibleValues())
{
if(qPossibleValueTranslator == null)
{
qPossibleValueTranslator = new QPossibleValueTranslator(getInput.getInstance(), getInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(getInput.getTable(), List.of(returnRecord));
}
if(getInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
}
return (returnRecord);
}
}

View File

@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
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.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
@ -48,6 +50,9 @@ public class InsertAction
*******************************************************************************/
public InsertOutput execute(InsertInput insertInput) throws QException
{
ActionHelper.validateSession(insertInput);
setAutomationStatusField(insertInput);
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
@ -57,6 +62,16 @@ public class InsertAction
/*******************************************************************************
** If the table being inserted into uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(InsertInput insertInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -95,15 +95,6 @@ public class QueryAction
records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t));
}
if(queryInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
if(queryInput.getShouldTranslatePossibleValues())
{
if(qPossibleValueTranslator == null)
@ -112,5 +103,14 @@ public class QueryAction
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records);
}
if(queryInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
}
}

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
@ -42,6 +44,7 @@ public class UpdateAction
public UpdateOutput execute(UpdateInput updateInput) throws QException
{
ActionHelper.validateSession(updateInput);
setAutomationStatusField(updateInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
@ -50,4 +53,15 @@ public class UpdateAction
// todo post-customization - can do whatever w/ the result if you want
return updateResult;
}
/*******************************************************************************
** If the table being updated uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(UpdateInput updateInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(updateInput.getTable(), updateInput.getRecords(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS);
}
}

View File

@ -33,6 +33,7 @@ import java.util.Objects;
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.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;
@ -41,6 +42,7 @@ 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;
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.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
@ -48,6 +50,7 @@ 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.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -89,7 +92,7 @@ public class QPossibleValueTranslator
*******************************************************************************/
public void translatePossibleValuesInRecords(QTableMetaData table, List<QRecord> records)
{
if(records == null)
if(records == null || table == null)
{
return;
}
@ -111,9 +114,41 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** Translate a list of ids to a list of possible values (e.g., w/ rendered values)
*******************************************************************************/
String translatePossibleValue(QFieldMetaData field, Serializable value)
public List<QPossibleValue<?>> buildTranslatedPossibleValueList(QPossibleValueSource possibleValueSource, List<Serializable> ids)
{
if(ids == null)
{
return (null);
}
if(ids.isEmpty())
{
return (new ArrayList<>());
}
List<QPossibleValue<?>> rs = new ArrayList<>();
if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
primePvsCache(possibleValueSource.getTableName(), List.of(possibleValueSource), ids);
}
for(Serializable id : ids)
{
String translated = translatePossibleValue(possibleValueSource, id);
rs.add(new QPossibleValue<>(id, translated));
}
return (rs);
}
/*******************************************************************************
** For a given field and (raw/id) value, get the translated (string) value.
*******************************************************************************/
public String translatePossibleValue(QFieldMetaData field, Serializable value)
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource == null)
@ -122,6 +157,31 @@ public class QPossibleValueTranslator
return (null);
}
try
{
if(field.getType().equals(QFieldType.INTEGER) && !(value instanceof Integer))
{
value = ValueUtils.getValueAsInteger(value);
}
}
catch(QValueException e)
{
LOG.info("Error translating possible value raw value...");
///////////////////////////
// leave value as it was //
///////////////////////////
}
return translatePossibleValue(possibleValueSource, value);
}
/*******************************************************************************
** For a given PossibleValueSource and (raw/id) value, get the translated (string) value.
*******************************************************************************/
String translatePossibleValue(QPossibleValueSource possibleValueSource, Serializable value)
{
String resultValue = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
@ -129,15 +189,15 @@ public class QPossibleValueTranslator
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
resultValue = translatePossibleValueTable(field, value, possibleValueSource);
resultValue = translatePossibleValueTable(value, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM))
{
resultValue = translatePossibleValueCustom(field, value, possibleValueSource);
resultValue = translatePossibleValueCustom(value, possibleValueSource);
}
else
{
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "] on field [" + field.getName() + "]");
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "]");
}
if(resultValue == null)
@ -151,7 +211,7 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** do translation for an enum-type PVS
*******************************************************************************/
private String translatePossibleValueEnum(Serializable value, QPossibleValueSource possibleValueSource)
{
@ -169,9 +229,9 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** do translation for a table-type PVS
*******************************************************************************/
private String translatePossibleValueTable(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
String translatePossibleValueTable(Serializable value, QPossibleValueSource possibleValueSource)
{
/////////////////////////////////
// null input gets null output //
@ -197,9 +257,9 @@ public class QPossibleValueTranslator
/*******************************************************************************
**
** do translation for a custom-type PVS
*******************************************************************************/
private String translatePossibleValueCustom(QFieldMetaData field, Serializable value, QPossibleValueSource possibleValueSource)
private String translatePossibleValueCustom(Serializable value, QPossibleValueSource possibleValueSource)
{
try
{
@ -208,7 +268,7 @@ public class QPossibleValueTranslator
}
catch(Exception e)
{
LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
LOG.warn("Error sending [" + value + "] for through custom code for PVS [" + possibleValueSource.getName() + "]", e);
}
return (null);
@ -294,13 +354,28 @@ public class QPossibleValueTranslator
{
for(QFieldMetaData field : fieldsByPvsTable.get(tableName))
{
values.add(record.getValue(field.getName()));
Serializable fieldValue = record.getValue(field.getName());
//////////////////////////////////////
// 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());
if(!cacheForPvs.containsKey(fieldValue))
{
values.add(fieldValue);
}
}
}
if(!values.isEmpty())
{
primePvsCache(tableName, pvsesByTable.get(tableName), values);
}
}
}
@ -331,7 +406,11 @@ public class QPossibleValueTranslator
/////////////////////////////////////////////////////////////////////////////////////////
// this is needed to get record labels, which are what we use here... unclear if best! //
/////////////////////////////////////////////////////////////////////////////////////////
if(notTooDeep())
{
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
}
QueryOutput queryOutput = new QueryAction().execute(queryInput);
@ -352,4 +431,24 @@ public class QPossibleValueTranslator
}
}
/*******************************************************************************
** Avoid infinite recursion, for where one field's PVS depends on another's...
** not too smart, just breaks at 5...
*******************************************************************************/
private boolean notTooDeep()
{
int count = 0;
for(StackTraceElement stackTraceElement : new Throwable().getStackTrace())
{
if(stackTraceElement.getMethodName().equals("translatePossibleValuesInRecords"))
{
count++;
}
}
return (count < 5);
}
}

View File

@ -23,9 +23,14 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.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;
@ -41,12 +46,53 @@ public class QValueFormatter
{
private static final Logger LOG = LogManager.getLogger(QValueFormatter.class);
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a");
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/*******************************************************************************
**
** For a field, and its value, apply the field's displayFormat.
*******************************************************************************/
public String formatValue(QFieldMetaData field, Serializable value)
{
if(QFieldType.BOOLEAN.equals(field.getType()))
{
Boolean b = ValueUtils.getValueAsBoolean(value);
if(b == null)
{
return (null);
}
else if(b)
{
return ("Yes");
}
else
{
return ("No");
}
}
return (formatValue(field.getDisplayFormat(), field.getName(), value));
}
/*******************************************************************************
** For a display format string (e.g., %d), and a value, apply the displayFormat.
*******************************************************************************/
public String formatValue(String displayFormat, Serializable value)
{
return (formatValue(displayFormat, "", value));
}
/*******************************************************************************
** 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)
{
//////////////////////////////////
// null values get null results //
@ -59,11 +105,11 @@ public class QValueFormatter
////////////////////////////////////////////////////////
// if the field has a display format, try to apply it //
////////////////////////////////////////////////////////
if(StringUtils.hasContent(field.getDisplayFormat()))
if(StringUtils.hasContent(displayFormat))
{
try
{
return (field.getDisplayFormat().formatted(value));
return (displayFormat.formatted(value));
}
catch(Exception e)
{
@ -72,20 +118,24 @@ public class QValueFormatter
// todo - revisit if we actually want this - or - if you should get an error if you mis-configure your table this way (ideally during validation!)
if(e.getMessage().equals("f != java.lang.Integer"))
{
return formatValue(field, ValueUtils.getValueAsBigDecimal(value));
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
}
else if(e.getMessage().equals("f != java.lang.String"))
{
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
}
else if(e.getMessage().equals("d != java.math.BigDecimal"))
{
return formatValue(field, ValueUtils.getValueAsInteger(value));
return formatValue(displayFormat, ValueUtils.getValueAsInteger(value));
}
else
{
LOG.warn("Error formatting value [" + value + "] for field [" + field.getName() + "] with format [" + field.getDisplayFormat() + "]: " + e.getMessage());
LOG.warn("Error formatting value [" + value + "] for field [" + fieldName + "] with format [" + displayFormat + "]: " + e.getMessage());
}
}
catch(Exception e2)
{
LOG.warn("Caught secondary exception trying to convert type on field [" + field.getName() + "] for formatting", e);
LOG.warn("Caught secondary exception trying to convert type on field [" + fieldName + "] for formatting", e);
}
}
}
@ -98,6 +148,26 @@ public class QValueFormatter
/*******************************************************************************
**
*******************************************************************************/
public String formatDate(LocalDate date)
{
return (dateFormatter.format(date));
}
/*******************************************************************************
**
*******************************************************************************/
public String formatDateTime(LocalDateTime dateTime)
{
return (dateTimeFormatter.format(dateTime));
}
/*******************************************************************************
** Make a string from a table's recordLabelFormat and fields, for a given record.
*******************************************************************************/
@ -108,25 +178,59 @@ public class QValueFormatter
return (formatRecordLabelExceptionalCases(table, record));
}
///////////////////////////////////////////////////////////////////////
// get list of values, then pass them to the string formatter method //
///////////////////////////////////////////////////////////////////////
try
{
List<Serializable> values = table.getRecordLabelFields().stream()
.map(record::getValue)
.map(v -> v == null ? "" : v)
.toList();
return (table.getRecordLabelFormat().formatted(values.toArray()));
return formatStringWithFields(table.getRecordLabelFormat(), table.getRecordLabelFields(), record.getDisplayValues(), record.getValues());
}
catch(Exception e)
{
LOG.debug("Error formatting record label", e);
return (formatRecordLabelExceptionalCases(table, record));
}
}
/*******************************************************************************
** 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)
{
List<Serializable> values = formatFields.stream()
.map(fieldName ->
{
///////////////////////////////////////////////////////////////////////////
// if there's a display value set, then use it. Else, use the raw value //
///////////////////////////////////////////////////////////////////////////
String displayValue = displayValueMap.get(fieldName);
if(displayValue != null)
{
return (displayValue);
}
return rawValueMap.get(fieldName);
})
.map(v -> v == null ? "" : v)
.toList();
return (formatString.formatted(values.toArray()));
}
/*******************************************************************************
** 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)
{
List<String> values = formatValues.stream()
.map(v -> v == null ? "" : v)
.toList();
return (formatString.formatted(values.toArray()));
}
/*******************************************************************************
** Deal with non-happy-path cases for making a record label.
*******************************************************************************/
@ -168,10 +272,13 @@ 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);
}
}
record.setRecordLabel(formatRecordLabel(table, record));
}

View File

@ -0,0 +1,244 @@
/*
* 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.values;
import java.io.Serializable;
import java.util.ArrayList;
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.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.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
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.model.metadata.possiblevalues.QPossibleValueSourceType;
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.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
** Class responsible for looking up possible-values for fields/records and
** make them into display values.
*******************************************************************************/
public class SearchPossibleValueSourceAction
{
private static final Logger LOG = LogManager.getLogger(SearchPossibleValueSourceAction.class);
private QPossibleValueTranslator possibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
public SearchPossibleValueSourceOutput execute(SearchPossibleValueSourceInput input) throws QException
{
QInstance qInstance = input.getInstance();
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(input.getPossibleValueSourceName());
if(possibleValueSource == null)
{
throw new QException("Missing possible value source named [" + input.getPossibleValueSourceName() + "]");
}
possibleValueTranslator = new QPossibleValueTranslator(input.getInstance(), input.getSession());
SearchPossibleValueSourceOutput output = null;
if(possibleValueSource.getType().equals(QPossibleValueSourceType.ENUM))
{
output = searchPossibleValueEnum(input, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
output = searchPossibleValueTable(input, possibleValueSource);
}
else if(possibleValueSource.getType().equals(QPossibleValueSourceType.CUSTOM))
{
output = searchPossibleValueCustom(input, possibleValueSource);
}
else
{
LOG.error("Unrecognized possibleValueSourceType [" + possibleValueSource.getType() + "] in PVS named [" + possibleValueSource.getName() + "]");
}
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueEnum(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
List<Serializable> matchingIds = new ArrayList<>();
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
boolean match = false;
if(input.getIdList() != null)
{
if(input.getIdList().contains(possibleValue.getId()))
{
match = true;
}
}
else
{
if(StringUtils.hasContent(input.getSearchTerm()))
{
match = (Objects.equals(ValueUtils.getValueAsString(possibleValue.getId()).toLowerCase(), input.getSearchTerm().toLowerCase())
|| possibleValue.getLabel().toLowerCase().startsWith(input.getSearchTerm().toLowerCase()));
}
else
{
match = true;
}
}
if(match)
{
matchingIds.add((Serializable) possibleValue.getId());
}
// todo - skip & limit?
// todo - default filter
}
List<QPossibleValue<?>> qPossibleValues = possibleValueTranslator.buildTranslatedPossibleValueList(possibleValueSource, matchingIds);
output.setResults(qPossibleValues);
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueTable(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource) throws QException
{
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
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)
{
queryFilter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList()));
}
else
{
if(StringUtils.hasContent(input.getSearchTerm()))
{
for(String valueField : possibleValueSource.getSearchFields())
{
QFieldMetaData field = table.getField(valueField);
if(field.getType().equals(QFieldType.STRING))
{
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm())));
}
else if(field.getType().equals(QFieldType.DATE) || field.getType().equals(QFieldType.DATE_TIME))
{
LOG.debug("Not querying PVS [" + possibleValueSource.getName() + "] on date field [" + field.getName() + "]");
// todo - what? queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.STARTS_WITH, List.of(input.getSearchTerm())));
}
else
{
try
{
Integer valueAsInteger = ValueUtils.getValueAsInteger(input.getSearchTerm());
if(valueAsInteger != null)
{
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.EQUALS, List.of(valueAsInteger)));
}
}
catch(Exception e)
{
////////////////////////////////////////////////////////
// write a FALSE criteria if the value isn't a number //
////////////////////////////////////////////////////////
queryFilter.addCriteria(new QFilterCriteria(valueField, QCriteriaOperator.IN, List.of()));
}
}
}
}
}
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
// todo - default filter
// todo - skip & limit as params
queryInput.setLimit(250);
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);
output.setResults(qPossibleValues);
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
private SearchPossibleValueSourceOutput searchPossibleValueCustom(SearchPossibleValueSourceInput input, QPossibleValueSource possibleValueSource)
{
try
{
// QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
// return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
}
catch(Exception e)
{
// LOG.warn("Error sending [" + value + "] for field [" + field + "] through custom code for PVS [" + field.getPossibleValueSourceName() + "]", e);
}
throw new NotImplementedException("Not impleemnted");
// return (null);
}
}

View File

@ -26,6 +26,7 @@ import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@ -62,8 +63,7 @@ public class CsvToQRecordAdapter
*******************************************************************************/
public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping, Consumer<QRecord> recordCustomizer)
{
this.recordPipe = recordPipe;
doBuildRecordsFromCsv(csv, table, mapping, recordCustomizer);
buildRecordsFromCsv(new InputWrapper().withRecordPipe(recordPipe).withCsv(csv).withTable(table).withMapping(mapping).withRecordCustomizer(recordCustomizer));
}
@ -75,8 +75,7 @@ public class CsvToQRecordAdapter
*******************************************************************************/
public List<QRecord> buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping)
{
this.recordList = new ArrayList<>();
doBuildRecordsFromCsv(csv, table, mapping, null);
buildRecordsFromCsv(new InputWrapper().withCsv(csv).withTable(table).withMapping(mapping));
return (recordList);
}
@ -88,13 +87,29 @@ public class CsvToQRecordAdapter
**
** todo - meta-data validation, type handling
*******************************************************************************/
public void doBuildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping<?> mapping, Consumer<QRecord> recordCustomizer)
public void buildRecordsFromCsv(InputWrapper inputWrapper)
{
String csv = inputWrapper.getCsv();
AbstractQFieldMapping<?> mapping = inputWrapper.getMapping();
Consumer<QRecord> recordCustomizer = inputWrapper.getRecordCustomizer();
QTableMetaData table = inputWrapper.getTable();
Integer limit = inputWrapper.getLimit();
if(!StringUtils.hasContent(csv))
{
throw (new IllegalArgumentException("Empty csv value was provided."));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if caller supplied a record pipe, use it -- but if it's null, then create a recordList to populate. //
// see addRecord method for usage. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
this.recordPipe = inputWrapper.getRecordPipe();
if(this.recordPipe == null)
{
this.recordList = new ArrayList<>();
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// once, from a DOS csv file (that had come from Excel), we had a "" character (FEFF, Byte-order marker) at the start of a //
// CSV, which caused our first header to not match... So, let us strip away any FEFF or FFFE's at the start of CSV strings. //
@ -120,16 +135,20 @@ public class CsvToQRecordAdapter
List<String> headers = csvParser.getHeaderNames();
headers = makeHeadersUnique(headers);
List<CSVRecord> csvRecords = csvParser.getRecords();
for(CSVRecord csvRecord : csvRecords)
Iterator<CSVRecord> csvIterator = csvParser.iterator();
int recordCount = 0;
while(csvIterator.hasNext())
{
CSVRecord csvRecord = csvIterator.next();
//////////////////////////////////////////////////////////////////
// put values from the CSV record into a map of header -> value //
//////////////////////////////////////////////////////////////////
Map<String, String> csvValues = new HashMap<>();
for(int i = 0; i < headers.size() && i < csvRecord.size(); i++)
{
csvValues.put(headers.get(i), csvRecord.get(i));
String header = adjustHeaderCase(headers.get(i), inputWrapper);
csvValues.put(header, csvRecord.get(i));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -139,11 +158,18 @@ public class CsvToQRecordAdapter
for(QFieldMetaData field : table.getFields().values())
{
String fieldSource = mapping == null ? field.getName() : String.valueOf(mapping.getFieldSource(field.getName()));
fieldSource = adjustHeaderCase(fieldSource, inputWrapper);
qRecord.setValue(field.getName(), csvValues.get(fieldSource));
}
runRecordCustomizer(recordCustomizer, qRecord);
addRecord(qRecord);
recordCount++;
if(limit != null && recordCount > limit)
{
break;
}
}
}
else if(AbstractQFieldMapping.SourceType.INDEX.equals(mapping.getSourceType()))
@ -155,9 +181,12 @@ public class CsvToQRecordAdapter
CSVFormat.DEFAULT
.withTrim());
List<CSVRecord> csvRecords = csvParser.getRecords();
for(CSVRecord csvRecord : csvRecords)
Iterator<CSVRecord> csvIterator = csvParser.iterator();
int recordCount = 0;
while(csvIterator.hasNext())
{
CSVRecord csvRecord = csvIterator.next();
/////////////////////////////////////////////////////////////////
// put values from the CSV record into a map of index -> value //
/////////////////////////////////////////////////////////////////
@ -180,6 +209,12 @@ public class CsvToQRecordAdapter
runRecordCustomizer(recordCustomizer, qRecord);
addRecord(qRecord);
recordCount++;
if(limit != null && recordCount > limit)
{
break;
}
}
}
else
@ -195,6 +230,20 @@ public class CsvToQRecordAdapter
/*******************************************************************************
**
*******************************************************************************/
private String adjustHeaderCase(String s, InputWrapper inputWrapper)
{
if(inputWrapper.caseSensitiveHeaders)
{
return (s);
}
return (s.toLowerCase());
}
/*******************************************************************************
**
*******************************************************************************/
@ -261,4 +310,277 @@ public class CsvToQRecordAdapter
}
}
/*******************************************************************************
** Getter for recordList - note - only is valid if you don't supply a pipe in
** the input. If you do supply a pipe, then you get an exception if you call here!
**
*******************************************************************************/
public List<QRecord> getRecordList()
{
if(recordPipe != null)
{
throw (new IllegalStateException("getRecordList called on a CSVToQRecordAdapter that ran with a recordPipe."));
}
return recordList;
}
/*******************************************************************************
**
*******************************************************************************/
public static class InputWrapper
{
private RecordPipe recordPipe;
private String csv;
private QTableMetaData table;
private AbstractQFieldMapping<?> mapping;
private Consumer<QRecord> recordCustomizer;
private Integer limit;
private boolean caseSensitiveHeaders = false;
/*******************************************************************************
** Getter for recordPipe
**
*******************************************************************************/
public RecordPipe getRecordPipe()
{
return recordPipe;
}
/*******************************************************************************
** Setter for recordPipe
**
*******************************************************************************/
public void setRecordPipe(RecordPipe recordPipe)
{
this.recordPipe = recordPipe;
}
/*******************************************************************************
** Fluent setter for recordPipe
**
*******************************************************************************/
public InputWrapper withRecordPipe(RecordPipe recordPipe)
{
this.recordPipe = recordPipe;
return (this);
}
/*******************************************************************************
** Getter for csv
**
*******************************************************************************/
public String getCsv()
{
return csv;
}
/*******************************************************************************
** Setter for csv
**
*******************************************************************************/
public void setCsv(String csv)
{
this.csv = csv;
}
/*******************************************************************************
** Fluent setter for csv
**
*******************************************************************************/
public InputWrapper withCsv(String csv)
{
this.csv = csv;
return (this);
}
/*******************************************************************************
** Getter for table
**
*******************************************************************************/
public QTableMetaData getTable()
{
return table;
}
/*******************************************************************************
** Setter for table
**
*******************************************************************************/
public void setTable(QTableMetaData table)
{
this.table = table;
}
/*******************************************************************************
** Fluent setter for table
**
*******************************************************************************/
public InputWrapper withTable(QTableMetaData table)
{
this.table = table;
return (this);
}
/*******************************************************************************
** Getter for mapping
**
*******************************************************************************/
public AbstractQFieldMapping<?> getMapping()
{
return mapping;
}
/*******************************************************************************
** Setter for mapping
**
*******************************************************************************/
public void setMapping(AbstractQFieldMapping<?> mapping)
{
this.mapping = mapping;
}
/*******************************************************************************
** Fluent setter for mapping
**
*******************************************************************************/
public InputWrapper withMapping(AbstractQFieldMapping<?> mapping)
{
this.mapping = mapping;
return (this);
}
/*******************************************************************************
** Getter for recordCustomizer
**
*******************************************************************************/
public Consumer<QRecord> getRecordCustomizer()
{
return recordCustomizer;
}
/*******************************************************************************
** Setter for recordCustomizer
**
*******************************************************************************/
public void setRecordCustomizer(Consumer<QRecord> recordCustomizer)
{
this.recordCustomizer = recordCustomizer;
}
/*******************************************************************************
** Fluent setter for recordCustomizer
**
*******************************************************************************/
public InputWrapper withRecordCustomizer(Consumer<QRecord> recordCustomizer)
{
this.recordCustomizer = recordCustomizer;
return (this);
}
/*******************************************************************************
** Getter for limit
**
*******************************************************************************/
public Integer getLimit()
{
return limit;
}
/*******************************************************************************
** Setter for limit
**
*******************************************************************************/
public void setLimit(Integer limit)
{
this.limit = limit;
}
/*******************************************************************************
** Fluent setter for limit
**
*******************************************************************************/
public InputWrapper withLimit(Integer limit)
{
this.limit = limit;
return (this);
}
/*******************************************************************************
** Getter for caseSensitiveHeaders
**
*******************************************************************************/
public boolean getCaseSensitiveHeaders()
{
return caseSensitiveHeaders;
}
/*******************************************************************************
** Setter for caseSensitiveHeaders
**
*******************************************************************************/
public void setCaseSensitiveHeaders(boolean caseSensitiveHeaders)
{
this.caseSensitiveHeaders = caseSensitiveHeaders;
}
/*******************************************************************************
** Fluent setter for caseSensitiveHeaders
**
*******************************************************************************/
public InputWrapper withCaseSensitiveHeaders(boolean caseSensitiveHeaders)
{
this.caseSensitiveHeaders = caseSensitiveHeaders;
return (this);
}
}
}

View File

@ -0,0 +1,79 @@
/*
* 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 while executing custom code in QQQ.
**
** Context field is meant to give the user "context" for where the error occurred
** - e.g., a line number or word that was bad.
*******************************************************************************/
public class QCodeException extends QException
{
private String context;
/*******************************************************************************
** Constructor of message
**
*******************************************************************************/
public QCodeException(String message)
{
super(message);
}
/*******************************************************************************
** Constructor of message & cause
**
*******************************************************************************/
public QCodeException(String message, Throwable cause)
{
super(message, cause);
}
/*******************************************************************************
** Getter for context
**
*******************************************************************************/
public String getContext()
{
return context;
}
/*******************************************************************************
** Setter for context
**
*******************************************************************************/
public void setContext(String context)
{
this.context = context;
}
}

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 while generating reports
*
*******************************************************************************/
public class QFormulaException extends QException
{
/*******************************************************************************
** Constructor of message
**
*******************************************************************************/
public QFormulaException(String message)
{
super(message);
}
/*******************************************************************************
** Constructor of message & cause
**
*******************************************************************************/
public QFormulaException(String message, Throwable cause)
{
super(message, cause);
}
}

View File

@ -22,38 +22,46 @@
package com.kingsrook.qqq.backend.core.instances;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
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.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
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.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.processes.QBackendStepMetaData;
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;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteStoreStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditReceiveValuesStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditStoreRecordsStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertReceiveFileStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertStoreRecordsStep;
import com.kingsrook.qqq.backend.core.processes.implementations.general.LoadInitialRecordsStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.edit.BulkEditTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.insert.BulkInsertTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaUpdateStep;
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;
@ -69,32 +77,59 @@ public class QInstanceEnricher
{
private static final Logger LOG = LogManager.getLogger(QInstanceEnricher.class);
private final QInstance qInstance;
//////////////////////////////////////////////////////////
// todo - come up w/ a way for app devs to set configs! //
//////////////////////////////////////////////////////////
private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true;
/*******************************************************************************
**
*******************************************************************************/
public void enrich(QInstance qInstance)
public QInstanceEnricher(QInstance qInstance)
{
this.qInstance = qInstance;
}
/*******************************************************************************
**
*******************************************************************************/
public void enrich()
{
if(qInstance.getTables() != null)
{
qInstance.getTables().values().forEach(this::enrich);
qInstance.getTables().values().forEach(this::enrichTable);
defineTableBulkProcesses(qInstance);
}
if(qInstance.getProcesses() != null)
{
qInstance.getProcesses().values().forEach(this::enrich);
qInstance.getProcesses().values().forEach(this::enrichProcess);
}
if(qInstance.getBackends() != null)
{
qInstance.getBackends().values().forEach(this::enrich);
qInstance.getBackends().values().forEach(this::enrichBackend);
}
if(qInstance.getApps() != null)
{
qInstance.getApps().values().forEach(this::enrich);
qInstance.getApps().values().forEach(this::enrichApp);
}
if(qInstance.getReports() != null)
{
qInstance.getReports().values().forEach(this::enrichReport);
}
if(qInstance.getPossibleValueSources() != null)
{
qInstance.getPossibleValueSources().values().forEach(this::enrichPossibleValueSource);
}
}
@ -103,7 +138,7 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void enrich(QBackendMetaData qBackendMetaData)
private void enrichBackend(QBackendMetaData qBackendMetaData)
{
qBackendMetaData.enrich();
}
@ -113,7 +148,7 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void enrich(QTableMetaData table)
private void enrichTable(QTableMetaData table)
{
if(!StringUtils.hasContent(table.getLabel()))
{
@ -122,13 +157,17 @@ public class QInstanceEnricher
if(table.getFields() != null)
{
table.getFields().values().forEach(this::enrich);
table.getFields().values().forEach(this::enrichField);
}
if(CollectionUtils.nullSafeIsEmpty(table.getSections()))
{
generateTableFieldSections(table);
}
else
{
table.getSections().forEach(this::enrichFieldSection);
}
if(CollectionUtils.nullSafeHasContents(table.getRecordLabelFields()) && !StringUtils.hasContent(table.getRecordLabelFormat()))
{
@ -141,7 +180,7 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void enrich(QProcessMetaData process)
private void enrichProcess(QProcessMetaData process)
{
if(!StringUtils.hasContent(process.getLabel()))
{
@ -150,7 +189,7 @@ public class QInstanceEnricher
if(process.getStepList() != null)
{
process.getStepList().forEach(this::enrich);
process.getStepList().forEach(this::enrichStep);
}
}
@ -159,29 +198,29 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void enrich(QStepMetaData step)
private void enrichStep(QStepMetaData step)
{
if(!StringUtils.hasContent(step.getLabel()))
{
step.setLabel(nameToLabel(step.getName()));
}
step.getInputFields().forEach(this::enrich);
step.getOutputFields().forEach(this::enrich);
step.getInputFields().forEach(this::enrichField);
step.getOutputFields().forEach(this::enrichField);
if(step instanceof QFrontendStepMetaData frontendStepMetaData)
{
if(frontendStepMetaData.getFormFields() != null)
{
frontendStepMetaData.getFormFields().forEach(this::enrich);
frontendStepMetaData.getFormFields().forEach(this::enrichField);
}
if(frontendStepMetaData.getViewFields() != null)
{
frontendStepMetaData.getViewFields().forEach(this::enrich);
frontendStepMetaData.getViewFields().forEach(this::enrichField);
}
if(frontendStepMetaData.getRecordListFields() != null)
{
frontendStepMetaData.getRecordListFields().forEach(this::enrich);
frontendStepMetaData.getRecordListFields().forEach(this::enrichField);
}
}
}
@ -191,25 +230,97 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
private void enrich(QFieldMetaData field)
private void enrichField(QFieldMetaData field)
{
if(!StringUtils.hasContent(field.getLabel()))
{
if(configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels && StringUtils.hasContent(field.getPossibleValueSourceName()) && field.getName() != null && field.getName().endsWith("Id"))
{
field.setLabel(nameToLabel(field.getName().substring(0, field.getName().length() - 2)));
}
else
{
field.setLabel(nameToLabel(field.getName()));
}
}
//////////////////////////////////////////////////////////////////////////
// if this field has a possibleValueSource //
// and that PVS exists in the instance //
// and it's a table-type PVS and the table name is set //
// and it's a valid table in the instance, and the table is in some app //
// and the field doesn't have a LINK adornment //
// then add a link-to-record-from-table adornment to the field. //
//////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource != null)
{
String tableName = possibleValueSource.getTableName();
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && StringUtils.hasContent(tableName))
{
if(qInstance.getTable(tableName) != null && doesAnyAppHaveTable(tableName))
{
if(field.getAdornments() == null || field.getAdornments().stream().noneMatch(a -> AdornmentType.LINK.equals(a.getType())))
{
field.withFieldAdornment(new FieldAdornment().withType(AdornmentType.LINK)
.withValue(AdornmentType.LinkValues.TO_RECORD_FROM_TABLE, tableName));
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void enrich(QAppMetaData app)
private boolean doesAnyAppHaveTable(String tableName)
{
if(qInstance.getApps() != null)
{
for(QAppMetaData app : qInstance.getApps().values())
{
if(app.getChildren() != null)
{
for(QAppChildMetaData child : app.getChildren())
{
if(child instanceof QTableMetaData && tableName.equals(child.getName()))
{
return (true);
}
}
}
}
}
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
private void enrichApp(QAppMetaData app)
{
if(!StringUtils.hasContent(app.getLabel()))
{
app.setLabel(nameToLabel(app.getName()));
}
if(CollectionUtils.nullSafeIsEmpty(app.getSections()))
{
generateAppSections(app);
}
for(QAppSection section : CollectionUtils.nonNullList(app.getSections()))
{
enrichAppSection(section);
}
}
@ -217,7 +328,51 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
static String nameToLabel(String name)
private void enrichAppSection(QAppSection section)
{
if(!StringUtils.hasContent(section.getLabel()))
{
section.setLabel(nameToLabel(section.getName()));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void enrichFieldSection(QFieldSection section)
{
if(!StringUtils.hasContent(section.getLabel()))
{
section.setLabel(nameToLabel(section.getName()));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void enrichReport(QReportMetaData report)
{
if(!StringUtils.hasContent(report.getLabel()))
{
report.setLabel(nameToLabel(report.getName()));
}
if(report.getInputFields() != null)
{
report.getInputFields().forEach(this::enrichField);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static String nameToLabel(String name)
{
if(!StringUtils.hasContent(name))
{
@ -293,6 +448,20 @@ public class QInstanceEnricher
*******************************************************************************/
private void defineTableBulkInsert(QInstance qInstance, QTableMetaData table, String processName)
{
Map<String, Serializable> values = new HashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
BulkInsertExtractStep.class,
BulkInsertTransformStep.class,
LoadViaInsertStep.class,
values
)
.withName(processName)
.withLabel(table.getLabel() + " Bulk Insert")
.withTableName(table.getName())
.withIsHidden(true);
List<QFieldMetaData> editableFields = table.getFields().values().stream()
.filter(QFieldMetaData::getIsEditable)
.toList();
@ -307,50 +476,13 @@ public class QInstanceEnricher
.withFormField(new QFieldMetaData("theFile", QFieldType.BLOB).withIsRequired(true))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
// .withValue("text", "Upload a CSV or XLSX file with the following columns: " + fieldsForHelpText));
.withValue("text", "Upload a CSV file with the following columns: " + fieldsForHelpText));
.withValue("previewText", "file upload instructions")
.withValue("text", "Upload a CSV file with the following columns:\n" + fieldsForHelpText))
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM));
QBackendStepMetaData receiveFileStep = new QBackendStepMetaData()
.withName("receiveFile")
.withCode(new QCodeReference(BulkInsertReceiveFileStep.class))
.withOutputMetaData(new QFunctionOutputMetaData()
.withFieldList(List.of(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER))));
QFrontendStepMetaData reviewScreen = new QFrontendStepMetaData()
.withName("review")
.withRecordListFields(editableFields)
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("text", "The records below were parsed from your file, and will be inserted if you click Submit."))
.withViewField(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER).withLabel("# of file rows"));
QBackendStepMetaData storeStep = new QBackendStepMetaData()
.withName("storeRecords")
.withCode(new QCodeReference(BulkInsertStoreRecordsStep.class))
.withOutputMetaData(new QFunctionOutputMetaData()
.withFieldList(List.of(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER))));
QFrontendStepMetaData resultsScreen = new QFrontendStepMetaData()
.withName("results")
.withRecordListFields(new ArrayList<>(table.getFields().values()))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("text", "The records below have been inserted."))
.withViewField(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER).withLabel("# of file rows"));
qInstance.addProcess(
new QProcessMetaData()
.withName(processName)
.withLabel(table.getLabel() + " Bulk Insert")
.withTableName(table.getName())
.withIsHidden(true)
.withStepList(List.of(
uploadScreen,
receiveFileStep,
reviewScreen,
storeStep,
resultsScreen
)));
process.addStep(0, uploadScreen);
process.getFrontendStep("review").setRecordListFields(editableFields);
qInstance.addProcess(process);
}
@ -360,6 +492,22 @@ public class QInstanceEnricher
*******************************************************************************/
private void defineTableBulkEdit(QInstance qInstance, QTableMetaData table, String processName)
{
Map<String, Serializable> values = new HashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, table.getName());
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, StreamedETLWithFrontendProcess.DEFAULT_PREVIEW_MESSAGE_FOR_UPDATE);
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
ExtractViaQueryStep.class,
BulkEditTransformStep.class,
LoadViaUpdateStep.class,
values
)
.withName(processName)
.withLabel(table.getLabel() + " Bulk Edit")
.withTableName(table.getName())
.withIsHidden(true);
List<QFieldMetaData> editableFields = table.getFields().values().stream()
.filter(QFieldMetaData::getIsEditable)
.toList();
@ -375,54 +523,11 @@ public class QInstanceEnricher
The values you supply here will be updated in all of the records you are bulk editing.
You can clear out the value in a field by flipping the switch on for that field and leaving the input field blank.
Fields whose switches are off will not be updated."""))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.BULK_EDIT_FORM)
);
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.BULK_EDIT_FORM));
QBackendStepMetaData receiveValuesStep = new QBackendStepMetaData()
.withName("receiveValues")
.withCode(new QCodeReference(BulkEditReceiveValuesStep.class))
.withInputData(new QFunctionInputMetaData()
.withRecordListMetaData(new QRecordListMetaData().withTableName(table.getName()))
.withField(new QFieldMetaData(BulkEditReceiveValuesStep.FIELD_ENABLED_FIELDS, QFieldType.STRING))
.withFields(editableFields));
QFrontendStepMetaData reviewScreen = new QFrontendStepMetaData()
.withName("review")
.withRecordListFields(editableFields)
.withViewField(new QFieldMetaData(BulkEditReceiveValuesStep.FIELD_VALUES_BEING_UPDATED, QFieldType.STRING))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("text", "The records below will be updated if you click Submit."));
QBackendStepMetaData storeStep = new QBackendStepMetaData()
.withName("storeRecords")
.withCode(new QCodeReference(BulkEditStoreRecordsStep.class))
.withOutputMetaData(new QFunctionOutputMetaData()
.withFieldList(List.of(new QFieldMetaData("noOfFileRows", QFieldType.INTEGER))));
QFrontendStepMetaData resultsScreen = new QFrontendStepMetaData()
.withName("results")
.withRecordListFields(new ArrayList<>(table.getFields().values()))
.withViewField(new QFieldMetaData(BulkEditReceiveValuesStep.FIELD_VALUES_BEING_UPDATED, QFieldType.STRING))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("text", "The records below have been updated."));
qInstance.addProcess(
new QProcessMetaData()
.withName(processName)
.withLabel(table.getLabel() + " Bulk Edit")
.withTableName(table.getName())
.withIsHidden(true)
.withStepList(List.of(
LoadInitialRecordsStep.defineMetaData(table.getName()),
editScreen,
receiveValuesStep,
reviewScreen,
storeStep,
resultsScreen
)));
process.addStep(0, editScreen);
process.getFrontendStep("review").setRecordListFields(editableFields);
qInstance.addProcess(process);
}
@ -432,36 +537,26 @@ public class QInstanceEnricher
*******************************************************************************/
private void defineTableBulkDelete(QInstance qInstance, QTableMetaData table, String processName)
{
QFrontendStepMetaData reviewScreen = new QFrontendStepMetaData()
.withName("review")
.withRecordListFields(new ArrayList<>(table.getFields().values()))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("text", "The records below will be deleted if you click Submit."));
Map<String, Serializable> values = new HashMap<>();
values.put(StreamedETLWithFrontendProcess.FIELD_SOURCE_TABLE, table.getName());
values.put(StreamedETLWithFrontendProcess.FIELD_DESTINATION_TABLE, table.getName());
values.put(StreamedETLWithFrontendProcess.FIELD_PREVIEW_MESSAGE, StreamedETLWithFrontendProcess.DEFAULT_PREVIEW_MESSAGE_FOR_DELETE);
QBackendStepMetaData storeStep = new QBackendStepMetaData()
.withName("delete")
.withCode(new QCodeReference(BulkDeleteStoreStep.class));
QFrontendStepMetaData resultsScreen = new QFrontendStepMetaData()
.withName("results")
.withRecordListFields(new ArrayList<>(table.getFields().values()))
.withComponent(new QFrontendComponentMetaData()
.withType(QComponentType.HELP_TEXT)
.withValue("text", "The records below have been deleted."));
qInstance.addProcess(
new QProcessMetaData()
QProcessMetaData process = StreamedETLWithFrontendProcess.defineProcessMetaData(
ExtractViaQueryStep.class,
BulkDeleteTransformStep.class,
LoadViaDeleteStep.class,
values
)
.withName(processName)
.withLabel(table.getLabel() + " Bulk Delete")
.withTableName(table.getName())
.withIsHidden(true)
.withStepList(List.of(
LoadInitialRecordsStep.defineMetaData(table.getName()),
reviewScreen,
storeStep,
resultsScreen
)));
.withIsHidden(true);
List<QFieldMetaData> tableFields = table.getFields().values().stream().toList();
process.getFrontendStep("review").setRecordListFields(tableFields);
qInstance.addProcess(process);
}
@ -513,7 +608,7 @@ public class QInstanceEnricher
** <li>TLAAndAnotherTLA -> tla_and_another_tla</li>
** </ul>
*******************************************************************************/
static String inferBackendName(String fieldName)
public static String inferBackendName(String fieldName)
{
////////////////////////////////////////////////////////////////////////////////////////
// build a list of words in the name, then join them with _ and lower-case the result //
@ -569,6 +664,60 @@ public class QInstanceEnricher
}
/*******************************************************************************
** If a app didn't have any sections, generate "sensible defaults"
*******************************************************************************/
private void generateAppSections(QAppMetaData app)
{
if(CollectionUtils.nullSafeIsEmpty(app.getChildren()))
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// assume this app is valid if it has no children, but surely it doesn't need any sections then. //
/////////////////////////////////////////////////////////////////////////////////////////////////
return;
}
//////////////////////////////////////////////////////////////////////////////
// create an identity section for the id and any fields in the record label //
//////////////////////////////////////////////////////////////////////////////
QAppSection defaultSection = new QAppSection(app.getName(), app.getLabel(), new QIcon("badge"), new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
boolean foundNonAppChild = false;
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
{
for(QAppChildMetaData child : app.getChildren())
{
//////////////////////////////////////////////////////////////////////////////////////////
// only tables, processes, and reports are allowed to be in sections at this time, apps //
// might be children but not in sections so keep track if we find any non-app //
//////////////////////////////////////////////////////////////////////////////////////////
if(child.getClass().equals(QTableMetaData.class))
{
defaultSection.getTables().add(child.getName());
foundNonAppChild = true;
}
else if(child.getClass().equals(QProcessMetaData.class))
{
defaultSection.getProcesses().add(child.getName());
foundNonAppChild = true;
}
else if(child.getClass().equals(QReportMetaData.class))
{
defaultSection.getReports().add(child.getName());
foundNonAppChild = true;
}
}
}
if(foundNonAppChild)
{
app.addSection(defaultSection);
}
}
/*******************************************************************************
** If a table didn't have any sections, generate "sensible defaults"
*******************************************************************************/
@ -634,4 +783,50 @@ public class QInstanceEnricher
}
}
/*******************************************************************************
**
*******************************************************************************/
private void enrichPossibleValueSource(QPossibleValueSource possibleValueSource)
{
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()))
{
if(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getSearchFields()))
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
if(table != null)
{
if(table.getPrimaryKeyField() != null)
{
possibleValueSource.withSearchField(table.getPrimaryKeyField());
}
for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields()))
{
possibleValueSource.withSearchField(recordLabelField);
}
}
}
if(CollectionUtils.nullSafeIsEmpty(possibleValueSource.getOrderByFields()))
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
if(table != null)
{
for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields()))
{
possibleValueSource.withOrderByField(recordLabelField);
}
if(table.getPrimaryKeyField() != null)
{
possibleValueSource.withOrderByField(table.getPrimaryKeyField());
}
}
}
}
}
}

View File

@ -22,10 +22,16 @@
package com.kingsrook.qqq.backend.core.instances;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
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.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;
@ -45,7 +51,23 @@ public class QMetaDataVariableInterpreter
{
private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class);
private Map<String, String> customEnvironment;
private Map<String, String> environmentOverrides;
private Map<String, Map<String, Serializable>> valueMaps;
/*******************************************************************************
**
*******************************************************************************/
public QMetaDataVariableInterpreter()
{
environmentOverrides = new HashMap<>();
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
for(DotenvEntry e : dotenv.entries())
{
environmentOverrides.put(e.getKey(), e.getValue());
}
}
@ -103,6 +125,18 @@ public class QMetaDataVariableInterpreter
/*******************************************************************************
** Interpret a value string, which may be a variable, into its run-time value -
** always as a String.
**
*******************************************************************************/
public String interpret(String value)
{
return (ValueUtils.getValueAsString(interpretForObject(value)));
}
/*******************************************************************************
** Interpret a value string, which may be a variable, into its run-time value.
**
@ -113,7 +147,28 @@ public class QMetaDataVariableInterpreter
** - used if you really want to get back the literal value, ${env.X}, for example.
** Else the output is the input.
*******************************************************************************/
public String interpret(String value)
public Serializable interpretForObject(String value)
{
return (interpretForObject(value, null));
}
/*******************************************************************************
** Interpret a value string, which may be a variable, into its run-time value,
** getting back the specified default if the string looks like a variable, but can't
** be found. Where "looks like" means, for example, started with "${env." and ended
** with "}", but wasn't set in the environment, or, more interestingly, based on the
** valueMaps - only if the name to the left of the dot is an actual valueMap name.
**
** If input is null, output is null.
** If input looks like ${env.X}, then the return value is the value of the env variable 'X'
** If input looks like ${prop.X}, then the return value is the value of the system property 'X'
** If input looks like ${literal.X}, then the return value is the literal 'X'
** - used if you really want to get back the literal value, ${env.X}, for example.
** Else the output is the input.
*******************************************************************************/
public Serializable interpretForObject(String value, Serializable defaultIfLooksLikeVariableButNotFound)
{
if(value == null)
{
@ -124,23 +179,48 @@ public class QMetaDataVariableInterpreter
if(value.startsWith(envPrefix) && value.endsWith("}"))
{
String envVarName = value.substring(envPrefix.length()).replaceFirst("}$", "");
String envValue = getEnvironment().get(envVarName);
return (envValue);
String result = getEnvironmentVariable(envVarName);
return (result == null ? defaultIfLooksLikeVariableButNotFound : result);
}
String propPrefix = "${prop.";
if(value.startsWith(propPrefix) && value.endsWith("}"))
{
String propertyName = value.substring(propPrefix.length()).replaceFirst("}$", "");
String propertyValue = System.getProperty(propertyName);
return (propertyValue);
String result = System.getProperty(propertyName);
return (result == null ? defaultIfLooksLikeVariableButNotFound : result);
}
String literalPrefix = "${literal.";
if(value.startsWith(literalPrefix) && value.endsWith("}"))
{
String literalValue = value.substring(literalPrefix.length()).replaceFirst("}$", "");
return (literalValue);
return (value.substring(literalPrefix.length()).replaceFirst("}$", ""));
}
if(valueMaps != null)
{
boolean looksLikeVariable = false;
for(Map.Entry<String, Map<String, Serializable>> entry : valueMaps.entrySet())
{
String name = entry.getKey();
Map<String, Serializable> valueMap = entry.getValue();
String prefix = "${" + name + ".";
if(value.startsWith(prefix) && value.endsWith("}"))
{
looksLikeVariable = true;
String lookupName = value.substring(prefix.length()).replaceFirst("}$", "");
if(valueMap != null && valueMap.containsKey(lookupName))
{
return (valueMap.get(lookupName));
}
}
}
if(looksLikeVariable)
{
return (defaultIfLooksLikeVariableButNotFound);
}
}
return (value);
@ -149,13 +229,13 @@ public class QMetaDataVariableInterpreter
/*******************************************************************************
** Setter for customEnvironment - protected - meant to be called (at least at this
** Setter for environmentOverrides - protected - meant to be called (at least at this
** time), only in unit test
**
*******************************************************************************/
protected void setCustomEnvironment(Map<String, String> customEnvironment)
protected void setEnvironmentOverrides(Map<String, String> environmentOverrides)
{
this.customEnvironment = customEnvironment;
this.environmentOverrides = environmentOverrides;
}
@ -163,13 +243,28 @@ public class QMetaDataVariableInterpreter
/*******************************************************************************
**
*******************************************************************************/
private Map<String, String> getEnvironment()
private String getEnvironmentVariable(String key)
{
if(this.customEnvironment != null)
if(this.environmentOverrides.containsKey(key))
{
return (this.customEnvironment);
return (this.environmentOverrides.get(key));
}
return System.getenv();
return System.getenv(key);
}
/*******************************************************************************
**
*******************************************************************************/
public void addValueMap(String name, Map<String, Serializable> values)
{
if(valueMaps == null)
{
valueMaps = new LinkedHashMap<>();
}
valueMaps.put(name, values);
}
}

View File

@ -25,10 +25,13 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
/*******************************************************************************
@ -39,9 +42,12 @@ public class MetaDataOutput extends AbstractActionOutput
{
private Map<String, QFrontendTableMetaData> tables;
private Map<String, QFrontendProcessMetaData> processes;
private Map<String, QFrontendReportMetaData> reports;
private Map<String, QFrontendAppMetaData> apps;
private Map<String, QFrontendWidgetMetaData> widgets;
private List<AppTreeNode> appTree;
private QBrandingMetaData branding;
@ -89,6 +95,27 @@ public class MetaDataOutput extends AbstractActionOutput
/*******************************************************************************
** Getter for reports
**
*******************************************************************************/
public Map<String, QFrontendReportMetaData> getReports()
{
return reports;
}
/*******************************************************************************
** Setter for reports
**
*******************************************************************************/
public void setReports(Map<String, QFrontendReportMetaData> reports)
{
this.reports = reports;
}
/*******************************************************************************
** Getter for appTree
@ -131,4 +158,48 @@ public class MetaDataOutput extends AbstractActionOutput
{
this.apps = apps;
}
/*******************************************************************************
** Getter for widgets
**
*******************************************************************************/
public Map<String, QFrontendWidgetMetaData> getWidgets()
{
return widgets;
}
/*******************************************************************************
** Setter for widgets
**
*******************************************************************************/
public void setWidgets(Map<String, QFrontendWidgetMetaData> widgets)
{
this.widgets = widgets;
}
/*******************************************************************************
** Getter for branding
**
*******************************************************************************/
public QBrandingMetaData getBranding()
{
return branding;
}
/*******************************************************************************
** Setter for branding
**
*******************************************************************************/
public void setBranding(QBrandingMetaData branding)
{
this.branding = branding;
}
}

View File

@ -38,6 +38,7 @@ public class ProcessState implements Serializable
{
private List<QRecord> records = new ArrayList<>();
private Map<String, Serializable> values = new HashMap<>();
private List<String> stepList = new ArrayList<>();
private Optional<String> nextStepName = Optional.empty();
@ -117,4 +118,25 @@ public class ProcessState implements Serializable
this.nextStepName = Optional.empty();
}
/*******************************************************************************
** Getter for stepList
**
*******************************************************************************/
public List<String> getStepList()
{
return stepList;
}
/*******************************************************************************
** Setter for stepList
**
*******************************************************************************/
public void setStepList(List<String> stepList)
{
this.stepList = stepList;
}
}

View File

@ -0,0 +1,457 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** For processes that may show a review & result screen, this class provides a
** standard way to summarize information about the records in the process.
**
*******************************************************************************/
public class ProcessSummaryLine implements ProcessSummaryLineInterface
{
private Status status;
private Integer count = 0;
private String message;
private String singularFutureMessage;
private String pluralFutureMessage;
private String singularPastMessage;
private String pluralPastMessage;
private String messageSuffix;
//////////////////////////////////////////////////////////////////////////
// using ArrayList, because we need to be Serializable, and List is not //
//////////////////////////////////////////////////////////////////////////
private ArrayList<Serializable> primaryKeys;
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryLine(Status status, Integer count, String message, ArrayList<Serializable> primaryKeys)
{
this.status = status;
this.count = count;
this.message = message;
this.primaryKeys = primaryKeys;
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryLine(Status status, Integer count, String message)
{
this.status = status;
this.count = count;
this.message = message;
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryLine(Status status, String message)
{
this.status = status;
this.message = message;
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryLine(Status status)
{
this.status = status;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "ProcessSummaryLine{status=" + status + ", count=" + count + ", message='" + message + '\'' + '}';
}
/*******************************************************************************
** Getter for status
**
*******************************************************************************/
public Status getStatus()
{
return status;
}
/*******************************************************************************
** Setter for status
**
*******************************************************************************/
public void setStatus(Status status)
{
this.status = status;
}
/*******************************************************************************
** Getter for primaryKeys
**
*******************************************************************************/
public List<Serializable> getPrimaryKeys()
{
return primaryKeys;
}
/*******************************************************************************
** Setter for primaryKeys
**
*******************************************************************************/
public void setPrimaryKeys(ArrayList<Serializable> primaryKeys)
{
this.primaryKeys = primaryKeys;
}
/*******************************************************************************
** Getter for count
**
*******************************************************************************/
public Integer getCount()
{
return count;
}
/*******************************************************************************
** Setter for count
**
*******************************************************************************/
public void setCount(Integer count)
{
this.count = count;
}
/*******************************************************************************
** Getter for message
**
*******************************************************************************/
public String getMessage()
{
return message;
}
/*******************************************************************************
** Setter for message
**
*******************************************************************************/
public void setMessage(String message)
{
this.message = message;
}
/*******************************************************************************
**
*******************************************************************************/
public void incrementCount()
{
incrementCount(1);
}
/*******************************************************************************
**
*******************************************************************************/
public void incrementCount(int amount)
{
if(count == null)
{
count = 0;
}
count += amount;
}
/*******************************************************************************
**
*******************************************************************************/
public void incrementCountAndAddPrimaryKey(Serializable primaryKey)
{
incrementCount();
if(primaryKeys == null)
{
primaryKeys = new ArrayList<>();
}
primaryKeys.add(primaryKey);
}
/*******************************************************************************
**
*******************************************************************************/
public void addSelfToListIfAnyCount(ArrayList<ProcessSummaryLineInterface> rs)
{
if(count != null && count > 0)
{
rs.add(this);
}
}
/*******************************************************************************
** Getter for singularFutureMessage
**
*******************************************************************************/
public String getSingularFutureMessage()
{
return singularFutureMessage;
}
/*******************************************************************************
** Setter for singularFutureMessage
**
*******************************************************************************/
public void setSingularFutureMessage(String singularFutureMessage)
{
this.singularFutureMessage = singularFutureMessage;
}
/*******************************************************************************
** Fluent setter for singularFutureMessage
**
*******************************************************************************/
public ProcessSummaryLine withSingularFutureMessage(String singularFutureMessage)
{
this.singularFutureMessage = singularFutureMessage;
return (this);
}
/*******************************************************************************
** Getter for pluralFutureMessage
**
*******************************************************************************/
public String getPluralFutureMessage()
{
return pluralFutureMessage;
}
/*******************************************************************************
** Setter for pluralFutureMessage
**
*******************************************************************************/
public void setPluralFutureMessage(String pluralFutureMessage)
{
this.pluralFutureMessage = pluralFutureMessage;
}
/*******************************************************************************
** Fluent setter for pluralFutureMessage
**
*******************************************************************************/
public ProcessSummaryLine withPluralFutureMessage(String pluralFutureMessage)
{
this.pluralFutureMessage = pluralFutureMessage;
return (this);
}
/*******************************************************************************
** Getter for singularPastMessage
**
*******************************************************************************/
public String getSingularPastMessage()
{
return singularPastMessage;
}
/*******************************************************************************
** Setter for singularPastMessage
**
*******************************************************************************/
public void setSingularPastMessage(String singularPastMessage)
{
this.singularPastMessage = singularPastMessage;
}
/*******************************************************************************
** Fluent setter for singularPastMessage
**
*******************************************************************************/
public ProcessSummaryLine withSingularPastMessage(String singularPastMessage)
{
this.singularPastMessage = singularPastMessage;
return (this);
}
/*******************************************************************************
** Getter for pluralPastMessage
**
*******************************************************************************/
public String getPluralPastMessage()
{
return pluralPastMessage;
}
/*******************************************************************************
** Setter for pluralPastMessage
**
*******************************************************************************/
public void setPluralPastMessage(String pluralPastMessage)
{
this.pluralPastMessage = pluralPastMessage;
}
/*******************************************************************************
** Fluent setter for pluralPastMessage
**
*******************************************************************************/
public ProcessSummaryLine withPluralPastMessage(String pluralPastMessage)
{
this.pluralPastMessage = pluralPastMessage;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void pickMessage(boolean isPast)
{
if(count != null)
{
if(count.equals(1))
{
setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
}
else
{
setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void prepareForFrontend(boolean isForResultScreen)
{
if(!StringUtils.hasContent(getMessage()))
{
pickMessage(isForResultScreen);
}
}
/*******************************************************************************
** Getter for messageSuffix
**
*******************************************************************************/
public String getMessageSuffix()
{
return messageSuffix;
}
/*******************************************************************************
** Setter for messageSuffix
**
*******************************************************************************/
public void setMessageSuffix(String messageSuffix)
{
this.messageSuffix = messageSuffix;
}
/*******************************************************************************
** Fluent setter for messageSuffix
**
*******************************************************************************/
public ProcessSummaryLine withMessageSuffix(String messageSuffix)
{
this.messageSuffix = messageSuffix;
return (this);
}
}

View File

@ -0,0 +1,50 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
/*******************************************************************************
** Interface for objects that can be output from a process to summarize its results.
*******************************************************************************/
public interface ProcessSummaryLineInterface extends Serializable
{
/*******************************************************************************
** Getter for status
**
*******************************************************************************/
Status getStatus();
/*******************************************************************************
** meant to be called by framework, after process is complete, give the
** summary object a chance to finalize itself before it's sent to a frontend.
*******************************************************************************/
default void prepareForFrontend(boolean isForResultScreen)
{
}
}

View File

@ -0,0 +1,280 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
/*******************************************************************************
** Simple process summary result object, that lets you give a link to a record
** in a table. e.g., if your process built such a record, give a link to it.
*******************************************************************************/
public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
{
private Status status;
private String tableName;
private Serializable recordId;
private String linkPreText;
private String linkText;
private String linkPostText;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ProcessSummaryRecordLink()
{
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryRecordLink(Status status, String tableName, Serializable recordId)
{
this.status = status;
this.tableName = tableName;
this.recordId = recordId;
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryRecordLink(Status status, String tableName, Serializable recordId, String linkText)
{
this.status = status;
this.tableName = tableName;
this.recordId = recordId;
this.linkText = linkText;
}
/*******************************************************************************
** Getter for status
**
*******************************************************************************/
public Status getStatus()
{
return status;
}
/*******************************************************************************
** Setter for status
**
*******************************************************************************/
public void setStatus(Status status)
{
this.status = status;
}
/*******************************************************************************
** Fluent setter for status
**
*******************************************************************************/
public ProcessSummaryRecordLink withStatus(Status status)
{
this.status = status;
return (this);
}
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Setter for tableName
**
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
**
*******************************************************************************/
public ProcessSummaryRecordLink withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for recordId
**
*******************************************************************************/
public Serializable getRecordId()
{
return recordId;
}
/*******************************************************************************
** Setter for recordId
**
*******************************************************************************/
public void setRecordId(Serializable recordId)
{
this.recordId = recordId;
}
/*******************************************************************************
** Fluent setter for recordId
**
*******************************************************************************/
public ProcessSummaryRecordLink withRecordId(Serializable recordId)
{
this.recordId = recordId;
return (this);
}
/*******************************************************************************
** Getter for linkPreText
**
*******************************************************************************/
public String getLinkPreText()
{
return linkPreText;
}
/*******************************************************************************
** Setter for linkPreText
**
*******************************************************************************/
public void setLinkPreText(String linkPreText)
{
this.linkPreText = linkPreText;
}
/*******************************************************************************
** Fluent setter for linkPreText
**
*******************************************************************************/
public ProcessSummaryRecordLink withLinkPreText(String linkPreText)
{
this.linkPreText = linkPreText;
return (this);
}
/*******************************************************************************
** Getter for linkText
**
*******************************************************************************/
public String getLinkText()
{
return linkText;
}
/*******************************************************************************
** Setter for linkText
**
*******************************************************************************/
public void setLinkText(String linkText)
{
this.linkText = linkText;
}
/*******************************************************************************
** Fluent setter for linkText
**
*******************************************************************************/
public ProcessSummaryRecordLink withLinkText(String linkText)
{
this.linkText = linkText;
return (this);
}
/*******************************************************************************
** Getter for linkPostText
**
*******************************************************************************/
public String getLinkPostText()
{
return linkPostText;
}
/*******************************************************************************
** Setter for linkPostText
**
*******************************************************************************/
public void setLinkPostText(String linkPostText)
{
this.linkPostText = linkPostText;
}
/*******************************************************************************
** Fluent setter for linkPostText
**
*******************************************************************************/
public ProcessSummaryRecordLink withLinkPostText(String linkPostText)
{
this.linkPostText = linkPostText;
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.time.Instant;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@ -50,6 +51,12 @@ public class RunBackendStepInput extends AbstractActionInput
private String stepName;
private QProcessCallback callback;
private AsyncJobCallback asyncJobCallback;
private RunProcessInput.FrontendStepBehavior frontendStepBehavior;
private Instant basepullLastRunTime;
////////////////////////////////////////////////////////////////////////////
// note - new fields should generally be added in method: cloneFieldsInto //
////////////////////////////////////////////////////////////////////////////
@ -85,6 +92,25 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
** Kinda like a reverse copy-constructor -- for a subclass that wants all the
** field values from this object. Keep this in sync with the fields in this class!
**
** Of note - the processState does NOT get cloned - because... well, in our first
** use-case (a subclass that doesn't WANT the same/full state), that's what we needed.
*******************************************************************************/
public void cloneFieldsInto(RunBackendStepInput target)
{
target.setStepName(getStepName());
target.setSession(getSession());
target.setTableName(getTableName());
target.setProcessName(getProcessName());
target.setAsyncJobCallback(getAsyncJobCallback());
target.setValues(getValues());
}
/*******************************************************************************
**
*******************************************************************************/
@ -354,7 +380,30 @@ public class RunBackendStepInput extends AbstractActionInput
*******************************************************************************/
public String getValueString(String fieldName)
{
return ((String) getValue(fieldName));
return (ValueUtils.getValueAsString(getValue(fieldName)));
}
/*******************************************************************************
** Getter for a single field's value
**
*******************************************************************************/
public Boolean getValueBoolean(String fieldName)
{
return (ValueUtils.getValueAsBoolean(getValue(fieldName)));
}
/*******************************************************************************
** Getter for a single field's value as a primitive boolean - with null => false.
**
*******************************************************************************/
public boolean getValuePrimitiveBoolean(String fieldName)
{
Boolean valueAsBoolean = ValueUtils.getValueAsBoolean(getValue(fieldName));
return (valueAsBoolean != null && valueAsBoolean);
}
@ -370,6 +419,17 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
** Getter for a single field's value
**
*******************************************************************************/
public Instant getValueInstant(String fieldName)
{
return (ValueUtils.getValueAsInstant(getValue(fieldName)));
}
/*******************************************************************************
** Accessor for processState - protected, because we generally want to access
** its members through wrapper methods, we think
@ -406,4 +466,73 @@ public class RunBackendStepInput extends AbstractActionInput
}
return (asyncJobCallback);
}
/*******************************************************************************
** Getter for frontendStepBehavior
**
*******************************************************************************/
public RunProcessInput.FrontendStepBehavior getFrontendStepBehavior()
{
return frontendStepBehavior;
}
/*******************************************************************************
** Setter for frontendStepBehavior
**
*******************************************************************************/
public void setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
{
this.frontendStepBehavior = frontendStepBehavior;
}
/*******************************************************************************
** Fluent setter for frontendStepBehavior
**
*******************************************************************************/
public RunBackendStepInput withFrontendStepBehavior(RunProcessInput.FrontendStepBehavior frontendStepBehavior)
{
this.frontendStepBehavior = frontendStepBehavior;
return (this);
}
/*******************************************************************************
** Getter for basepullLastRunTime
**
*******************************************************************************/
public Instant getBasepullLastRunTime()
{
return basepullLastRunTime;
}
/*******************************************************************************
** Setter for basepullLastRunTime
**
*******************************************************************************/
public void setBasepullLastRunTime(Instant basepullLastRunTime)
{
this.basepullLastRunTime = basepullLastRunTime;
}
/*******************************************************************************
** Fluent setter for basepullLastRunTime
**
*******************************************************************************/
public RunBackendStepInput withBasepullLastRunTime(Instant basepullLastRunTime)
{
this.basepullLastRunTime = basepullLastRunTime;
return (this);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
@ -241,4 +242,18 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
return (ValueUtils.getValueAsBigDecimal(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public void addRecord(QRecord record)
{
if(this.processState.getRecords() == null)
{
this.processState.setRecords(new ArrayList<>());
}
this.processState.getRecords().add(record);
}
}

View File

@ -0,0 +1,34 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.processes;
/*******************************************************************************
** Simple status enum - initially for statuses in process status lines.
*******************************************************************************/
public enum Status
{
OK,
WARNING,
ERROR,
INFO
}

View File

@ -0,0 +1,252 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.reporting;
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;
/*******************************************************************************
** Input for an Export action
*******************************************************************************/
public class ExportInput extends AbstractTableActionInput
{
private QQueryFilter queryFilter;
private Integer limit;
private List<String> fieldNames;
private String filename;
private ReportFormat reportFormat;
private OutputStream reportOutputStream;
private String titleRow;
private boolean includeHeaderRow = true;
/*******************************************************************************
**
*******************************************************************************/
public ExportInput()
{
}
/*******************************************************************************
**
*******************************************************************************/
public ExportInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public ExportInput(QInstance instance, QSession session)
{
super(instance);
setSession(session);
}
/*******************************************************************************
** Getter for queryFilter
**
*******************************************************************************/
public QQueryFilter getQueryFilter()
{
return queryFilter;
}
/*******************************************************************************
** Setter for queryFilter
**
*******************************************************************************/
public void setQueryFilter(QQueryFilter queryFilter)
{
this.queryFilter = queryFilter;
}
/*******************************************************************************
** Getter for limit
**
*******************************************************************************/
public Integer getLimit()
{
return limit;
}
/*******************************************************************************
** Setter for limit
**
*******************************************************************************/
public void setLimit(Integer limit)
{
this.limit = limit;
}
/*******************************************************************************
** Getter for fieldNames
**
*******************************************************************************/
public List<String> getFieldNames()
{
return fieldNames;
}
/*******************************************************************************
** Setter for fieldNames
**
*******************************************************************************/
public void setFieldNames(List<String> fieldNames)
{
this.fieldNames = fieldNames;
}
/*******************************************************************************
** Getter for filename
**
*******************************************************************************/
public String getFilename()
{
return filename;
}
/*******************************************************************************
** Setter for filename
**
*******************************************************************************/
public void setFilename(String filename)
{
this.filename = filename;
}
/*******************************************************************************
** Getter for reportFormat
**
*******************************************************************************/
public ReportFormat getReportFormat()
{
return reportFormat;
}
/*******************************************************************************
** Setter for reportFormat
**
*******************************************************************************/
public void setReportFormat(ReportFormat reportFormat)
{
this.reportFormat = reportFormat;
}
/*******************************************************************************
** Getter for reportOutputStream
**
*******************************************************************************/
public OutputStream getReportOutputStream()
{
return reportOutputStream;
}
/*******************************************************************************
** Setter for reportOutputStream
**
*******************************************************************************/
public void setReportOutputStream(OutputStream reportOutputStream)
{
this.reportOutputStream = reportOutputStream;
}
/*******************************************************************************
**
*******************************************************************************/
public String getTitleRow()
{
return titleRow;
}
/*******************************************************************************
**
*******************************************************************************/
public void setTitleRow(String titleRow)
{
this.titleRow = titleRow;
}
/*******************************************************************************
** Getter for includeHeaderRow
**
*******************************************************************************/
public boolean getIncludeHeaderRow()
{
return includeHeaderRow;
}
/*******************************************************************************
** Setter for includeHeaderRow
**
*******************************************************************************/
public void setIncludeHeaderRow(boolean includeHeaderRow)
{
this.includeHeaderRow = includeHeaderRow;
}
}

View File

@ -26,9 +26,9 @@ import java.io.Serializable;
/*******************************************************************************
** Output for a Report action
** Output for an Export action
*******************************************************************************/
public class ReportOutput implements Serializable
public class ExportOutput implements Serializable
{
public long recordCount;

View File

@ -24,9 +24,10 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting;
import java.util.Locale;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.reporting.CsvReportStreamer;
import com.kingsrook.qqq.backend.core.actions.reporting.ExcelReportStreamer;
import com.kingsrook.qqq.backend.core.actions.reporting.ReportStreamerInterface;
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.ListOfMapsExportStreamer;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.dhatim.fastexcel.Worksheet;
@ -37,22 +38,23 @@ import org.dhatim.fastexcel.Worksheet;
*******************************************************************************/
public enum ReportFormat
{
XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelReportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
CSV(null, null, CsvReportStreamer::new, "text/csv");
XLSX(Worksheet.MAX_ROWS, Worksheet.MAX_COLS, ExcelExportStreamer::new, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
CSV(null, null, CsvExportStreamer::new, "text/csv"),
LIST_OF_MAPS(null, null, ListOfMapsExportStreamer::new, null);
private final Integer maxRows;
private final Integer maxCols;
private final String mimeType;
private final Supplier<? extends ReportStreamerInterface> streamerConstructor;
private final Supplier<? extends ExportStreamerInterface> streamerConstructor;
/*******************************************************************************
**
*******************************************************************************/
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ReportStreamerInterface> streamerConstructor, String mimeType)
ReportFormat(Integer maxRows, Integer maxCols, Supplier<? extends ExportStreamerInterface> streamerConstructor, String mimeType)
{
this.maxRows = maxRows;
this.maxCols = maxCols;
@ -94,6 +96,7 @@ public enum ReportFormat
}
/*******************************************************************************
** Getter for maxCols
**
@ -119,7 +122,7 @@ public enum ReportFormat
/*******************************************************************************
**
*******************************************************************************/
public ReportStreamerInterface newReportStreamer()
public ExportStreamerInterface newReportStreamer()
{
return (streamerConstructor.get());
}

View File

@ -23,21 +23,20 @@ package com.kingsrook.qqq.backend.core.model.actions.reporting;
import java.io.OutputStream;
import java.util.List;
import java.io.Serializable;
import java.util.Map;
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;
/*******************************************************************************
** Input for a Report action
** Input for an Export action
*******************************************************************************/
public class ReportInput extends AbstractTableActionInput
{
private QQueryFilter queryFilter;
private Integer limit;
private List<String> fieldNames;
private String reportName;
private Map<String, Serializable> inputValues;
private String filename;
private ReportFormat reportFormat;
@ -76,67 +75,45 @@ public class ReportInput extends AbstractTableActionInput
/*******************************************************************************
** Getter for queryFilter
** Getter for reportName
**
*******************************************************************************/
public QQueryFilter getQueryFilter()
public String getReportName()
{
return queryFilter;
return reportName;
}
/*******************************************************************************
** Setter for queryFilter
** Setter for reportName
**
*******************************************************************************/
public void setQueryFilter(QQueryFilter queryFilter)
public void setReportName(String reportName)
{
this.queryFilter = queryFilter;
this.reportName = reportName;
}
/*******************************************************************************
** Getter for limit
** Getter for inputValues
**
*******************************************************************************/
public Integer getLimit()
public Map<String, Serializable> getInputValues()
{
return limit;
return inputValues;
}
/*******************************************************************************
** Setter for limit
** Setter for inputValues
**
*******************************************************************************/
public void setLimit(Integer limit)
public void setInputValues(Map<String, Serializable> inputValues)
{
this.limit = limit;
}
/*******************************************************************************
** Getter for fieldNames
**
*******************************************************************************/
public List<String> getFieldNames()
{
return fieldNames;
}
/*******************************************************************************
** Setter for fieldNames
**
*******************************************************************************/
public void setFieldNames(List<String> fieldNames)
{
this.fieldNames = fieldNames;
this.inputValues = inputValues;
}

View File

@ -0,0 +1,223 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
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;
/*******************************************************************************
**
*******************************************************************************/
public class ExecuteCodeInput extends AbstractActionInput
{
private QCodeReference codeReference;
private Map<String, Serializable> input;
private Map<String, Serializable> context;
private QCodeExecutionLoggerInterface executionLogger;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ExecuteCodeInput(QInstance qInstance)
{
super(qInstance);
}
/*******************************************************************************
** Getter for codeReference
**
*******************************************************************************/
public QCodeReference getCodeReference()
{
return codeReference;
}
/*******************************************************************************
** Setter for codeReference
**
*******************************************************************************/
public void setCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
**
*******************************************************************************/
public ExecuteCodeInput withCodeReference(QCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Getter for input
**
*******************************************************************************/
public Map<String, Serializable> getInput()
{
return input;
}
/*******************************************************************************
** Setter for input
**
*******************************************************************************/
public void setInput(Map<String, Serializable> input)
{
this.input = input;
}
/*******************************************************************************
** Fluent setter for input
**
*******************************************************************************/
public ExecuteCodeInput withInput(Map<String, Serializable> input)
{
this.input = input;
return (this);
}
/*******************************************************************************
** Fluent setter for input
**
*******************************************************************************/
public ExecuteCodeInput withInput(String key, Serializable value)
{
if(this.input == null)
{
input = new HashMap<>();
}
this.input.put(key, value);
return (this);
}
/*******************************************************************************
** Getter for context
**
*******************************************************************************/
public Map<String, Serializable> getContext()
{
return context;
}
/*******************************************************************************
** Setter for context
**
*******************************************************************************/
public void setContext(Map<String, Serializable> context)
{
this.context = context;
}
/*******************************************************************************
** Fluent setter for context
**
*******************************************************************************/
public ExecuteCodeInput withContext(Map<String, Serializable> context)
{
this.context = context;
return (this);
}
/*******************************************************************************
** Fluent setter for context
**
*******************************************************************************/
public ExecuteCodeInput withContext(String key, Serializable value)
{
if(this.context == null)
{
context = new HashMap<>();
}
this.context.put(key, value);
return (this);
}
/*******************************************************************************
** Getter for executionLogger
**
*******************************************************************************/
public QCodeExecutionLoggerInterface getExecutionLogger()
{
return executionLogger;
}
/*******************************************************************************
** Setter for executionLogger
**
*******************************************************************************/
public void setExecutionLogger(QCodeExecutionLoggerInterface executionLogger)
{
this.executionLogger = executionLogger;
}
/*******************************************************************************
** Fluent setter for executionLogger
**
*******************************************************************************/
public ExecuteCodeInput withExecutionLogger(QCodeExecutionLoggerInterface executionLogger)
{
this.executionLogger = executionLogger;
return (this);
}
}

View File

@ -0,0 +1,69 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
/*******************************************************************************
**
*******************************************************************************/
public class ExecuteCodeOutput
{
private Serializable output;
/*******************************************************************************
** Getter for output
**
*******************************************************************************/
public Serializable getOutput()
{
return output;
}
/*******************************************************************************
** Setter for output
**
*******************************************************************************/
public void setOutput(Serializable output)
{
this.output = output;
}
/*******************************************************************************
** Fluent setter for output
**
*******************************************************************************/
public ExecuteCodeOutput withOutput(Serializable output)
{
this.output = output;
return (this);
}
}

View File

@ -0,0 +1,154 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.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.AssociatedScriptCodeReference;
/*******************************************************************************
**
*******************************************************************************/
public class RunAssociatedScriptInput extends AbstractTableActionInput
{
private AssociatedScriptCodeReference codeReference;
private Map<String, Serializable> inputValues;
private Serializable outputObject;
/*******************************************************************************
**
*******************************************************************************/
public RunAssociatedScriptInput(QInstance qInstance)
{
super(qInstance);
}
/*******************************************************************************
** Getter for codeReference
**
*******************************************************************************/
public AssociatedScriptCodeReference getCodeReference()
{
return codeReference;
}
/*******************************************************************************
** Setter for codeReference
**
*******************************************************************************/
public void setCodeReference(AssociatedScriptCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
**
*******************************************************************************/
public RunAssociatedScriptInput withCodeReference(AssociatedScriptCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Getter for inputValues
**
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return inputValues;
}
/*******************************************************************************
** Setter for inputValues
**
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
**
*******************************************************************************/
public RunAssociatedScriptInput withInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
return (this);
}
/*******************************************************************************
** Getter for outputObject
**
*******************************************************************************/
public Serializable getOutputObject()
{
return outputObject;
}
/*******************************************************************************
** Setter for outputObject
**
*******************************************************************************/
public void setOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
}
/*******************************************************************************
** Fluent setter for outputObject
**
*******************************************************************************/
public RunAssociatedScriptInput withOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
return (this);
}
}

View File

@ -0,0 +1,70 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
**
*******************************************************************************/
public class RunAssociatedScriptOutput extends AbstractActionOutput
{
private Serializable output;
/*******************************************************************************
** Getter for output
**
*******************************************************************************/
public Serializable getOutput()
{
return output;
}
/*******************************************************************************
** Setter for output
**
*******************************************************************************/
public void setOutput(Serializable output)
{
this.output = output;
}
/*******************************************************************************
** Fluent setter for output
**
*******************************************************************************/
public RunAssociatedScriptOutput withOutput(Serializable output)
{
this.output = output;
return (this);
}
}

View File

@ -0,0 +1,188 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
**
*******************************************************************************/
public class StoreAssociatedScriptInput extends AbstractTableActionInput
{
private String fieldName;
private Serializable recordPrimaryKey;
private String code;
private String commitMessage;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public StoreAssociatedScriptInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for fieldName
**
*******************************************************************************/
public String getFieldName()
{
return fieldName;
}
/*******************************************************************************
** Setter for fieldName
**
*******************************************************************************/
public void setFieldName(String fieldName)
{
this.fieldName = fieldName;
}
/*******************************************************************************
** Fluent setter for fieldName
**
*******************************************************************************/
public StoreAssociatedScriptInput withFieldName(String fieldName)
{
this.fieldName = fieldName;
return (this);
}
/*******************************************************************************
** Getter for recordPrimaryKey
**
*******************************************************************************/
public Serializable getRecordPrimaryKey()
{
return recordPrimaryKey;
}
/*******************************************************************************
** Setter for recordPrimaryKey
**
*******************************************************************************/
public void setRecordPrimaryKey(Serializable recordPrimaryKey)
{
this.recordPrimaryKey = recordPrimaryKey;
}
/*******************************************************************************
** Fluent setter for recordPrimaryKey
**
*******************************************************************************/
public StoreAssociatedScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey)
{
this.recordPrimaryKey = recordPrimaryKey;
return (this);
}
/*******************************************************************************
** Getter for code
**
*******************************************************************************/
public String getCode()
{
return code;
}
/*******************************************************************************
** Setter for code
**
*******************************************************************************/
public void setCode(String code)
{
this.code = code;
}
/*******************************************************************************
** Fluent setter for code
**
*******************************************************************************/
public StoreAssociatedScriptInput withCode(String code)
{
this.code = code;
return (this);
}
/*******************************************************************************
** Getter for commitMessage
**
*******************************************************************************/
public String getCommitMessage()
{
return commitMessage;
}
/*******************************************************************************
** Setter for commitMessage
**
*******************************************************************************/
public void setCommitMessage(String commitMessage)
{
this.commitMessage = commitMessage;
}
/*******************************************************************************
** Fluent setter for commitMessage
**
*******************************************************************************/
public StoreAssociatedScriptInput withCommitMessage(String commitMessage)
{
this.commitMessage = commitMessage;
return (this);
}
}

View File

@ -0,0 +1,33 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
**
*******************************************************************************/
public class StoreAssociatedScriptOutput extends AbstractActionOutput
{
}

View File

@ -0,0 +1,187 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.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;
/*******************************************************************************
**
*******************************************************************************/
public class TestScriptInput extends AbstractTableActionInput
{
private Serializable recordPrimaryKey;
private String code;
private Serializable scriptTypeId;
private Map<String, String> inputValues;
/*******************************************************************************
**
*******************************************************************************/
public TestScriptInput(QInstance qInstance)
{
super(qInstance);
}
/*******************************************************************************
** Getter for recordPrimaryKey
**
*******************************************************************************/
public Serializable getRecordPrimaryKey()
{
return recordPrimaryKey;
}
/*******************************************************************************
** Setter for recordPrimaryKey
**
*******************************************************************************/
public void setRecordPrimaryKey(Serializable recordPrimaryKey)
{
this.recordPrimaryKey = recordPrimaryKey;
}
/*******************************************************************************
** Fluent setter for recordPrimaryKey
**
*******************************************************************************/
public TestScriptInput withRecordPrimaryKey(Serializable recordPrimaryKey)
{
this.recordPrimaryKey = recordPrimaryKey;
return (this);
}
/*******************************************************************************
** Getter for inputValues
**
*******************************************************************************/
public Map<String, String> getInputValues()
{
return inputValues;
}
/*******************************************************************************
** Setter for inputValues
**
*******************************************************************************/
public void setInputValues(Map<String, String> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
**
*******************************************************************************/
public TestScriptInput withInputValues(Map<String, String> inputValues)
{
this.inputValues = inputValues;
return (this);
}
/*******************************************************************************
** Getter for code
**
*******************************************************************************/
public String getCode()
{
return code;
}
/*******************************************************************************
** Setter for code
**
*******************************************************************************/
public void setCode(String code)
{
this.code = code;
}
/*******************************************************************************
** Fluent setter for code
**
*******************************************************************************/
public TestScriptInput withCode(String code)
{
this.code = code;
return (this);
}
/*******************************************************************************
** Getter for scriptTypeId
**
*******************************************************************************/
public Serializable getScriptTypeId()
{
return scriptTypeId;
}
/*******************************************************************************
** Setter for scriptTypeId
**
*******************************************************************************/
public void setScriptTypeId(Serializable scriptTypeId)
{
this.scriptTypeId = scriptTypeId;
}
/*******************************************************************************
** Fluent setter for scriptTypeId
**
*******************************************************************************/
public TestScriptInput withScriptTypeId(Serializable scriptTypeId)
{
this.scriptTypeId = scriptTypeId;
return (this);
}
}

View File

@ -0,0 +1,33 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
**
*******************************************************************************/
public class TestScriptOutput extends AbstractActionOutput
{
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.delete;
import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -35,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
*******************************************************************************/
public class DeleteInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private List<Serializable> primaryKeys;
private QQueryFilter queryFilter;
@ -59,6 +61,40 @@ public class DeleteInput extends AbstractTableActionInput
/*******************************************************************************
** Getter for transaction
**
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return transaction;
}
/*******************************************************************************
** Setter for transaction
**
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
**
*******************************************************************************/
public DeleteInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
/*******************************************************************************
** Getter for ids
**
@ -92,6 +128,7 @@ public class DeleteInput extends AbstractTableActionInput
}
/*******************************************************************************
** Getter for queryFilter
**
@ -113,6 +150,7 @@ public class DeleteInput extends AbstractTableActionInput
}
/*******************************************************************************
** Fluent setter for queryFilter
**

View File

@ -0,0 +1,186 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** Input data for the Get action
**
*******************************************************************************/
public class GetInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private Serializable primaryKey;
private boolean shouldTranslatePossibleValues = false;
private boolean shouldGenerateDisplayValues = false;
/*******************************************************************************
**
*******************************************************************************/
public GetInput()
{
}
/*******************************************************************************
**
*******************************************************************************/
public GetInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public GetInput(QInstance instance, QSession session)
{
super(instance);
setSession(session);
}
/*******************************************************************************
** Getter for primaryKey
**
*******************************************************************************/
public Serializable getPrimaryKey()
{
return primaryKey;
}
/*******************************************************************************
** Setter for primaryKey
**
*******************************************************************************/
public void setPrimaryKey(Serializable primaryKey)
{
this.primaryKey = primaryKey;
}
/*******************************************************************************
** Fluent setter for primaryKey
**
*******************************************************************************/
public GetInput withPrimaryKey(Serializable primaryKey)
{
this.primaryKey = primaryKey;
return (this);
}
/*******************************************************************************
** Getter for shouldTranslatePossibleValues
**
*******************************************************************************/
public boolean getShouldTranslatePossibleValues()
{
return shouldTranslatePossibleValues;
}
/*******************************************************************************
** Setter for shouldTranslatePossibleValues
**
*******************************************************************************/
public void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues)
{
this.shouldTranslatePossibleValues = shouldTranslatePossibleValues;
}
/*******************************************************************************
** Getter for shouldGenerateDisplayValues
**
*******************************************************************************/
public boolean getShouldGenerateDisplayValues()
{
return shouldGenerateDisplayValues;
}
/*******************************************************************************
** Setter for shouldGenerateDisplayValues
**
*******************************************************************************/
public void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues)
{
this.shouldGenerateDisplayValues = shouldGenerateDisplayValues;
}
/*******************************************************************************
** Getter for transaction
**
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return transaction;
}
/*******************************************************************************
** Setter for transaction
**
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
**
*******************************************************************************/
public GetInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -0,0 +1,72 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Output for a Get action
**
*******************************************************************************/
public class GetOutput extends AbstractActionOutput implements Serializable
{
private QRecord record;
/*******************************************************************************
** Getter for record
**
*******************************************************************************/
public QRecord getRecord()
{
return record;
}
/*******************************************************************************
** Setter for record
**
*******************************************************************************/
public void setRecord(QRecord record)
{
this.record = record;
}
/*******************************************************************************
** Fluent setter for record
**
*******************************************************************************/
public GetOutput withRecord(QRecord record)
{
this.record = record;
return (this);
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -54,4 +55,19 @@ public class InsertOutput extends AbstractActionOutput
{
this.records = records;
}
/*******************************************************************************
**
*******************************************************************************/
public void addRecord(QRecord record)
{
if(this.records == null)
{
this.records = new ArrayList<>();
}
this.records.add(record);
}
}

View File

@ -23,21 +23,51 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
* A single criteria Component of a Query
*
*******************************************************************************/
public class QFilterCriteria implements Serializable
public class QFilterCriteria implements Serializable, Cloneable
{
private static final Logger LOG = LogManager.getLogger(QFilterCriteria.class);
private String fieldName;
private QCriteriaOperator operator;
private List<Serializable> values;
/*******************************************************************************
**
*******************************************************************************/
@Override
public QFilterCriteria clone()
{
try
{
QFilterCriteria clone = (QFilterCriteria) super.clone();
if(values != null)
{
clone.values = new ArrayList<>();
clone.values.addAll(values);
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -158,4 +188,46 @@ public class QFilterCriteria implements Serializable
this.values = values;
return this;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
StringBuilder rs = new StringBuilder(fieldName);
try
{
rs.append(" ").append(operator).append(" ");
if(CollectionUtils.nullSafeHasContents(values))
{
if(values.size() == 1)
{
rs.append(values.get(0));
}
else
{
int index = 0;
for(Serializable value : values)
{
if(index++ > 9)
{
rs.append("and ").append(values.size() - index).append(" more");
break;
}
rs.append(value).append(",");
}
}
}
}
catch(Exception e)
{
LOG.warn("Error in toString", e);
rs.append("Error generating toString...");
}
return (rs.toString());
}
}

View File

@ -29,13 +29,62 @@ import java.io.Serializable;
** Bean representing an element of a query order-by clause.
**
*******************************************************************************/
public class QFilterOrderBy implements Serializable
public class QFilterOrderBy implements Serializable, Cloneable
{
private String fieldName;
private boolean isAscending = true;
/*******************************************************************************
**
*******************************************************************************/
@Override
public QFilterOrderBy clone()
{
try
{
return (QFilterOrderBy) super.clone();
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/*******************************************************************************
** Default no-arg constructor
*******************************************************************************/
public QFilterOrderBy()
{
}
/*******************************************************************************
** Constructor that sets field name, but leaves default for isAscending (true)
*******************************************************************************/
public QFilterOrderBy(String fieldName)
{
this.fieldName = fieldName;
}
/*******************************************************************************
** Constructor that takes field name and isAscending.
*******************************************************************************/
public QFilterOrderBy(String fieldName, boolean isAscending)
{
this.fieldName = fieldName;
this.isAscending = isAscending;
}
/*******************************************************************************
** Getter for fieldName
**
@ -102,4 +151,14 @@ public class QFilterOrderBy implements Serializable
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return (fieldName + " " + (isAscending ? "ASC" : "DESC"));
}
}

View File

@ -25,17 +25,109 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
* Full "filter" for a query - a list of criteria and order-bys
*
*******************************************************************************/
public class QQueryFilter implements Serializable
public class QQueryFilter implements Serializable, Cloneable
{
private static final Logger LOG = LogManager.getLogger(QQueryFilter.class);
private List<QFilterCriteria> criteria = new ArrayList<>();
private List<QFilterOrderBy> orderBys = new ArrayList<>();
private BooleanOperator booleanOperator = BooleanOperator.AND;
private List<QQueryFilter> subFilters = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public enum BooleanOperator
{
AND,
OR
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QQueryFilter clone()
{
try
{
QQueryFilter clone = (QQueryFilter) super.clone();
if(criteria != null)
{
clone.criteria = new ArrayList<>();
for(QFilterCriteria criterion : criteria)
{
clone.criteria.add(criterion.clone());
}
}
if(orderBys != null)
{
clone.orderBys = new ArrayList<>();
for(QFilterOrderBy orderBy : orderBys)
{
clone.orderBys.add(orderBy.clone());
}
}
if(subFilters != null)
{
clone.subFilters = new ArrayList<>();
for(QQueryFilter subFilter : subFilters)
{
clone.subFilters.add(subFilter.clone());
}
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/*******************************************************************************
**
*******************************************************************************/
public boolean hasAnyCriteria()
{
if(CollectionUtils.nullSafeHasContents(criteria))
{
return (true);
}
if(CollectionUtils.nullSafeHasContents(subFilters))
{
for(QQueryFilter subFilter : subFilters)
{
if(subFilter.hasAnyCriteria())
{
return (true);
}
}
}
return (false);
}
/*******************************************************************************
@ -130,4 +222,124 @@ public class QQueryFilter implements Serializable
return (this);
}
/*******************************************************************************
** Getter for booleanOperator
**
*******************************************************************************/
public BooleanOperator getBooleanOperator()
{
return booleanOperator;
}
/*******************************************************************************
** Setter for booleanOperator
**
*******************************************************************************/
public void setBooleanOperator(BooleanOperator booleanOperator)
{
this.booleanOperator = booleanOperator;
}
/*******************************************************************************
** Fluent setter for booleanOperator
**
*******************************************************************************/
public QQueryFilter withBooleanOperator(BooleanOperator booleanOperator)
{
this.booleanOperator = booleanOperator;
return (this);
}
/*******************************************************************************
** Getter for subFilters
**
*******************************************************************************/
public List<QQueryFilter> getSubFilters()
{
return subFilters;
}
/*******************************************************************************
** Setter for subFilters
**
*******************************************************************************/
public void setSubFilters(List<QQueryFilter> subFilters)
{
this.subFilters = subFilters;
}
/*******************************************************************************
** Fluent setter for subFilters
**
*******************************************************************************/
public QQueryFilter withSubFilters(List<QQueryFilter> subFilters)
{
this.subFilters = subFilters;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addSubFilter(QQueryFilter subFilter)
{
if(this.subFilters == null)
{
subFilters = new ArrayList<>();
}
subFilters.add(subFilter);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
StringBuilder rs = new StringBuilder("(");
try
{
for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria))
{
rs.append(criterion).append(" ").append(getBooleanOperator());
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters))
{
rs.append(subFilter);
}
rs.append(")");
rs.append("OrderBy[");
for(QFilterOrderBy orderBy : orderBys)
{
rs.append(orderBy).append(",");
}
rs.append("]");
}
catch(Exception e)
{
LOG.warn("Error in toString", e);
rs.append("Error generating toString...");
}
return (rs.toString());
}
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -34,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
*******************************************************************************/
public class QueryInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private QQueryFilter filter;
private Integer skip;
private Integer limit;
@ -44,6 +46,7 @@ public class QueryInput extends AbstractTableActionInput
private boolean shouldGenerateDisplayValues = false;
/*******************************************************************************
**
*******************************************************************************/
@ -203,4 +206,39 @@ public class QueryInput extends AbstractTableActionInput
{
this.shouldGenerateDisplayValues = shouldGenerateDisplayValues;
}
/*******************************************************************************
** Getter for transaction
**
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return transaction;
}
/*******************************************************************************
** Setter for transaction
**
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
**
*******************************************************************************/
public QueryInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -34,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
*******************************************************************************/
public class UpdateInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private List<QRecord> records;
////////////////////////////////////////////////////////////////////////////////////////////
@ -65,6 +67,40 @@ public class UpdateInput extends AbstractTableActionInput
/*******************************************************************************
** Getter for transaction
**
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return transaction;
}
/*******************************************************************************
** Setter for transaction
**
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
**
*******************************************************************************/
public UpdateInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
/*******************************************************************************
** Getter for records
**

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
@ -54,4 +55,18 @@ public class UpdateOutput extends AbstractActionOutput
{
this.records = records;
}
/*******************************************************************************
**
*******************************************************************************/
public void addRecord(QRecord record)
{
if(this.records == null)
{
this.records = new ArrayList<>();
}
this.records.add(record);
}
}

View File

@ -0,0 +1,268 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.values;
import java.io.Serializable;
import java.util.List;
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.metadata.QInstance;
/*******************************************************************************
** Input for the Search possible value source action
*******************************************************************************/
public class SearchPossibleValueSourceInput extends AbstractActionInput
{
private String possibleValueSourceName;
private QQueryFilter defaultQueryFilter;
private String searchTerm;
private List<Serializable> idList;
private Integer skip = 0;
private Integer limit = 100;
/*******************************************************************************
**
*******************************************************************************/
public SearchPossibleValueSourceInput()
{
}
/*******************************************************************************
**
*******************************************************************************/
public SearchPossibleValueSourceInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for possibleValueSourceName
**
*******************************************************************************/
public String getPossibleValueSourceName()
{
return possibleValueSourceName;
}
/*******************************************************************************
** Setter for possibleValueSourceName
**
*******************************************************************************/
public void setPossibleValueSourceName(String possibleValueSourceName)
{
this.possibleValueSourceName = possibleValueSourceName;
}
/*******************************************************************************
** Fluent setter for possibleValueSourceName
**
*******************************************************************************/
public SearchPossibleValueSourceInput withPossibleValueSourceName(String possibleValueSourceName)
{
this.possibleValueSourceName = possibleValueSourceName;
return (this);
}
/*******************************************************************************
** Getter for defaultQueryFilter
**
*******************************************************************************/
public QQueryFilter getDefaultQueryFilter()
{
return defaultQueryFilter;
}
/*******************************************************************************
** Setter for defaultQueryFilter
**
*******************************************************************************/
public void setDefaultQueryFilter(QQueryFilter defaultQueryFilter)
{
this.defaultQueryFilter = defaultQueryFilter;
}
/*******************************************************************************
** Fluent setter for defaultQueryFilter
**
*******************************************************************************/
public SearchPossibleValueSourceInput withDefaultQueryFilter(QQueryFilter defaultQueryFilter)
{
this.defaultQueryFilter = defaultQueryFilter;
return (this);
}
/*******************************************************************************
** Getter for searchTerm
**
*******************************************************************************/
public String getSearchTerm()
{
return searchTerm;
}
/*******************************************************************************
** Setter for searchTerm
**
*******************************************************************************/
public void setSearchTerm(String searchTerm)
{
this.searchTerm = searchTerm;
}
/*******************************************************************************
** Fluent setter for searchTerm
**
*******************************************************************************/
public SearchPossibleValueSourceInput withSearchTerm(String searchTerm)
{
this.searchTerm = searchTerm;
return (this);
}
/*******************************************************************************
** Getter for idList
**
*******************************************************************************/
public List<Serializable> getIdList()
{
return idList;
}
/*******************************************************************************
** Setter for idList
**
*******************************************************************************/
public void setIdList(List<Serializable> idList)
{
this.idList = idList;
}
/*******************************************************************************
** Fluent setter for idList
**
*******************************************************************************/
public SearchPossibleValueSourceInput withIdList(List<Serializable> idList)
{
this.idList = idList;
return (this);
}
/*******************************************************************************
** Getter for skip
**
*******************************************************************************/
public Integer getSkip()
{
return skip;
}
/*******************************************************************************
** Setter for skip
**
*******************************************************************************/
public void setSkip(Integer skip)
{
this.skip = skip;
}
/*******************************************************************************
** Fluent setter for skip
**
*******************************************************************************/
public SearchPossibleValueSourceInput withSkip(Integer skip)
{
this.skip = skip;
return (this);
}
/*******************************************************************************
** Getter for limit
**
*******************************************************************************/
public Integer getLimit()
{
return limit;
}
/*******************************************************************************
** Setter for limit
**
*******************************************************************************/
public void setLimit(Integer limit)
{
this.limit = limit;
}
/*******************************************************************************
** Fluent setter for limit
**
*******************************************************************************/
public SearchPossibleValueSourceInput withLimit(Integer limit)
{
this.limit = limit;
return (this);
}
}

View File

@ -0,0 +1,91 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.values;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
/*******************************************************************************
** Output for the Search possible value source action
*******************************************************************************/
public class SearchPossibleValueSourceOutput extends AbstractActionOutput
{
private List<QPossibleValue<?>> results = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public SearchPossibleValueSourceOutput()
{
}
/*******************************************************************************
**
*******************************************************************************/
public void addResult(QPossibleValue<?> possibleValue)
{
results.add(possibleValue);
}
/*******************************************************************************
** Getter for results
**
*******************************************************************************/
public List<QPossibleValue<?>> getResults()
{
return results;
}
/*******************************************************************************
** Setter for results
**
*******************************************************************************/
public void setResults(List<QPossibleValue<?>> results)
{
this.results = results;
}
/*******************************************************************************
** Fluent setter for results
**
*******************************************************************************/
public SearchPossibleValueSourceOutput withResults(List<QPossibleValue<?>> results)
{
this.results = results;
return (this);
}
}

View File

@ -0,0 +1,171 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.widgets;
import java.util.HashMap;
import java.util.Map;
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.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** Input data container for the RenderWidget action
**
*******************************************************************************/
public class RenderWidgetInput extends AbstractActionInput
{
private QSession session;
private QWidgetMetaDataInterface widgetMetaData;
private Map<String, String> queryParams;
/*******************************************************************************
**
*******************************************************************************/
public RenderWidgetInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for session
**
*******************************************************************************/
public QSession getSession()
{
return session;
}
/*******************************************************************************
** Setter for session
**
*******************************************************************************/
public void setSession(QSession session)
{
this.session = session;
}
/*******************************************************************************
** Fluent setter for session
**
*******************************************************************************/
public RenderWidgetInput withSession(QSession session)
{
this.session = session;
return (this);
}
/*******************************************************************************
** Getter for widgetMetaData
**
*******************************************************************************/
public QWidgetMetaDataInterface getWidgetMetaData()
{
return widgetMetaData;
}
/*******************************************************************************
** Setter for widgetMetaData
**
*******************************************************************************/
public void setWidgetMetaData(QWidgetMetaDataInterface widgetMetaData)
{
this.widgetMetaData = widgetMetaData;
}
/*******************************************************************************
** Fluent setter for widgetMetaData
**
*******************************************************************************/
public RenderWidgetInput withWidgetMetaData(QWidgetMetaDataInterface widgetMetaData)
{
this.widgetMetaData = widgetMetaData;
return (this);
}
/*******************************************************************************
** Getter for urlParams
**
*******************************************************************************/
public Map<String, String> getQueryParams()
{
return queryParams;
}
/*******************************************************************************
** Setter for urlParams
**
*******************************************************************************/
public void setQueryParams(Map<String, String> queryParams)
{
this.queryParams = queryParams;
}
/*******************************************************************************
** Fluent setter for urlParams
**
*******************************************************************************/
public RenderWidgetInput withUrlParams(Map<String, String> urlParams)
{
this.queryParams = urlParams;
return (this);
}
/*******************************************************************************
** adds a query param value
**
*******************************************************************************/
public void addQueryParam(String name, String value)
{
if(this.queryParams == null)
{
this.queryParams = new HashMap<>();
}
this.queryParams.put(name, value);
}
}

View File

@ -0,0 +1,80 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.widgets;
import java.io.Serializable;
/*******************************************************************************
** Output for an Export action
*******************************************************************************/
public class RenderWidgetOutput implements Serializable
{
public Object widgetData;
/*******************************************************************************
** constructor taking in widget data
**
*******************************************************************************/
public RenderWidgetOutput(Object widgetData)
{
this.widgetData = widgetData;
}
/*******************************************************************************
** Getter for widgetData
**
*******************************************************************************/
public Object getWidgetData()
{
return widgetData;
}
/*******************************************************************************
** Setter for widgetData
**
*******************************************************************************/
public void setWidgetData(Object widgetData)
{
this.widgetData = widgetData;
}
/*******************************************************************************
** Fluent setter for widgetData
**
*******************************************************************************/
public RenderWidgetOutput withWidgetData(Object widgetData)
{
this.widgetData = widgetData;
return (this);
}
}

Some files were not shown because too many files have changed in this diff Show More