Compare commits

..

120 Commits

Author SHA1 Message Date
acb311ee52 Make robust in case called twice 2023-08-05 20:09:11 -05:00
4a53c0d42a Add call for qqq tables meta data; 2023-08-05 20:08:58 -05:00
fa5c4f715f Also move audits to use qqqTable instead of auditTable 2023-08-05 20:03:35 -05:00
7a56d1ae22 Rename fields from qqqTableId to just tableId... 2023-08-05 19:46:19 -05:00
1d309afbea Update test utils meta data - add qqqTable 2023-08-05 11:42:26 -05:00
02fc031e09 passing tests w/ scriptId everywhere 2023-08-04 19:54:02 -05:00
e57292f10f Initial work to change scripts, tableTriggers, and savedFilters to use qqqTableId instead of tableName 2023-08-04 16:42:42 -05:00
b4a63e6e1b Updating to 0.18.0 2023-08-03 12:28:00 -05:00
0d78555a05 Merge tag 'version-0.17.0' into dev
Tag release
2023-08-03 12:27:56 -05:00
6a1db1c533 Merge branch 'release/0.17.0' 2023-08-03 12:25:49 -05:00
fabde303ab Update for next development version 2023-08-03 12:21:47 -05:00
6d173d5485 Update versions for release 2023-08-03 12:21:01 -05:00
79ac48b7f9 Merge branch 'integration/sprint-30' into dev 2023-08-03 11:59:20 -05:00
f7c8513845 CE-564 - Adding support for override warehouse and carrier service. 2023-08-03 11:43:06 -05:00
be30422c18 CE-564 - Adding support for override warehouse and carrier service. 2023-08-03 11:33:45 -05:00
53c005051e CE-537 - Updating to support API Delete 2023-08-03 11:16:24 -05:00
3879d5412c Merge pull request #35 from Kingsrook/feature/CE-548-script-writer-dev-setup-intelli-j-ide-local-ide-unit-testing
Feature/ce 548 script writer dev setup intelli j ide local ide unit testing
2023-08-01 18:49:23 -05:00
d596346c44 Merge pull request #34 from Kingsrook/dev
dev into sprint-30
2023-08-01 18:46:48 -05:00
ac88def08c CE-548 Update to handle process that aren't tied to a (single) table, but still take ids as input (e.g,. runScript) 2023-08-01 18:44:03 -05:00
29bb7252e8 CE-548 Add option to not includeUUIDs in logs 2023-08-01 09:16:09 -05:00
f0bd6b4b80 CE-548 Add override of addApiUtilityToContext 2023-08-01 09:15:45 -05:00
726075f041 CE-548 Add System.out script execution logger 2023-08-01 09:15:18 -05:00
67a1afdc1a CE-548 add some support for a single file's contents being submitted under input key "contents" (e.g., when used via API). 2023-08-01 09:11:59 -05:00
c832028961 Implement CHILD_POINTS_AT_PARENT use-case 2023-08-01 08:57:24 -05:00
774309e846 Add percents to ColumnStats 2023-07-27 08:37:05 -05:00
a19a516fc0 Merge pull request #33 from Kingsrook/feature/CE-551-change-logic-for-fed-ex
Feature/ce 551 change logic for fed ex
2023-07-26 08:42:47 -05:00
e153d3a7b4 Merge pull request #32 from Kingsrook/dev
dev into sprint-30
2023-07-26 08:42:11 -05:00
34a1755e44 CE-551 Add defaultValue to frontend field meta data 2023-07-25 13:06:42 -05:00
b4a2ba9582 Make implement TopLevelMetaDataInterface 2023-07-25 08:25:54 -05:00
9bb6600a9d Move default sort order to constant; add comment 'small runs first' 2023-07-25 08:25:38 -05:00
4f081e7c79 Split up PVS definition methods (in case an instance needs some (for scripts), but not all (not doing api log)); add some non-null checks around version lists 2023-07-25 08:25:17 -05:00
a0a43d48f5 Initial checkin (went with query timeout, but was missed) 2023-07-25 08:24:21 -05:00
7c4e06abcc Merge pull request #31 from Kingsrook/feature/query-timeout-and-cancel
Feature/query timeout and cancel
2023-07-25 08:14:09 -05:00
39d714fbb1 Updating to 0.17.0 2023-07-24 15:21:50 -05:00
6975069049 Merge tag 'version-0.16.0' into dev
Tag release
2023-07-24 15:21:46 -05:00
f05759ae83 Merge branch 'release/0.16.0' 2023-07-24 15:19:57 -05:00
81e4d5d36d Update for next development version 2023-07-24 15:17:13 -05:00
bff8a0f78a Update versions for release 2023-07-24 15:16:37 -05:00
87fbbc797a Merge pull request #26 from Kingsrook/dependabot/maven/qqq-middleware-picocli/com.h2database-h2-2.2.220
Bump h2 from 2.1.210 to 2.2.220 in /qqq-middleware-picocli
2023-07-24 14:45:53 -05:00
742caba8d2 Merge pull request #27 from Kingsrook/dependabot/maven/qqq-sample-project/com.h2database-h2-2.2.220
Bump h2 from 2.1.212 to 2.2.220 in /qqq-sample-project
2023-07-24 14:45:20 -05:00
f6f5a07d3a Merge pull request #28 from Kingsrook/dependabot/maven/qqq-backend-module-rdbms/com.h2database-h2-2.2.220
Bump h2 from 2.1.214 to 2.2.220 in /qqq-backend-module-rdbms
2023-07-24 14:44:47 -05:00
bc50c1e22e Merge pull request #29 from Kingsrook/dependabot/maven/qqq-middleware-javalin/com.h2database-h2-2.2.220
Bump h2 from 2.1.214 to 2.2.220 in /qqq-middleware-javalin
2023-07-24 14:44:13 -05:00
71672d46ee Initial checkin 2023-07-20 20:11:46 -05:00
75c84cd0ff Added constants referenced in last commit 2023-07-20 20:10:31 -05:00
0ff98ce7ea Add internal timeouts to RDBMS query, count, and aggregate, with timeoutSeconds field on their inputs; also add cancel method on those 3 actions, implemented down in RDBMS as well (e.g., to cancel inresponse to http request being abandoned) 2023-07-20 20:10:03 -05:00
c53f5e935d Merge pull request #30 from Kingsrook/integration/sprint-29
Integration/sprint 29
2023-07-20 09:57:34 -05:00
f5f2cc5007 CE-508 - Updated to support setCredentialsInHeader 2023-07-20 09:32:52 -05:00
d62a4c6daf CE-536 Add getRecordByUniqueKey 2023-07-18 16:28:32 -05:00
367fa4f657 Merge branch 'feature/datetime-query-expressions' into integration/sprint-29 2023-07-17 16:26:48 -05:00
5a5c9a0072 Revert: CE-536 If records are supplied to the process input, then use them instead of running a query. 2023-07-17 12:16:45 -05:00
2db1adc9ab CE-536 If records are supplied to the process input, then use them instead of running a query. 2023-07-17 11:34:01 -05:00
3d2708da23 CE-535 All more points of overridability, and make keys in existing record map a pair of {fieldName,value} 2023-07-14 14:08:27 -05:00
6ec838c48b Merge branch 'feature/CE-535-make-ip-a-wms-that-ct-live-oms-can-control' into integration/sprint-29 2023-07-14 11:32:42 -05:00
ebb7e7ab45 Try to add hints about unrecognized field names (if they're in other api versions) 2023-07-13 17:10:42 -05:00
8c3648920d Don't audit values for masked field types 2023-07-13 17:09:41 -05:00
6d6510c223 Add swapMultiLevelMapKeys 2023-07-13 17:09:18 -05:00
2422d09c31 Stop doing criteria expressions as their own thing, and instead put them in the values list 2023-07-13 09:17:34 -05:00
c04ab42bd9 CE-534: updates to support direct carrier tracker 2023-07-12 21:09:18 -05:00
c003d448d6 updates from last sprint's story 2023-07-12 21:07:20 -05:00
de8d668ea2 CE-535 new replace action, with test 2023-07-11 08:40:25 -05:00
953d97c554 CE-535 add auditContext 2023-07-11 08:31:28 -05:00
086787a5ca CE-535 Cleanup in DeleteAction - add omitDmlAudit & auditContext; be more sure not to delete associations if errors 2023-07-11 08:31:15 -05:00
e6816174c3 Test for APIRecordUtils.jsonQueryStyleQRecordToJSONObject 2023-07-10 11:07:06 -05:00
ed60ad2a96 Add doCopySourcePrimaryKeyToCache option - to, copy primary keys from source to cache; apply this in qqq-table cache 2023-07-10 09:49:11 -05:00
a943628e84 Update to flush buffered pipes - fixes issue where static data supplier records may not appear 2023-07-10 09:47:50 -05:00
5cfcb420d0 CE-535 Initial checkin 2023-07-10 09:46:13 -05:00
593c9f25f9 Bump h2 from 2.1.214 to 2.2.220 in /qqq-middleware-javalin
Bumps [h2](https://github.com/h2database/h2database) from 2.1.214 to 2.2.220.
- [Release notes](https://github.com/h2database/h2database/releases)
- [Commits](https://github.com/h2database/h2database/compare/version-2.1.214...version-2.2.220)

---
updated-dependencies:
- dependency-name: com.h2database:h2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 21:59:44 +00:00
ca560c933d Bump h2 from 2.1.214 to 2.2.220 in /qqq-backend-module-rdbms
Bumps [h2](https://github.com/h2database/h2database) from 2.1.214 to 2.2.220.
- [Release notes](https://github.com/h2database/h2database/releases)
- [Commits](https://github.com/h2database/h2database/compare/version-2.1.214...version-2.2.220)

---
updated-dependencies:
- dependency-name: com.h2database:h2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 21:59:09 +00:00
51dd0b6b29 Bump h2 from 2.1.212 to 2.2.220 in /qqq-sample-project
Bumps [h2](https://github.com/h2database/h2database) from 2.1.212 to 2.2.220.
- [Release notes](https://github.com/h2database/h2database/releases)
- [Commits](https://github.com/h2database/h2database/compare/version-2.1.212...version-2.2.220)

---
updated-dependencies:
- dependency-name: com.h2database:h2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 21:57:48 +00:00
b924fdcffa Bump h2 from 2.1.210 to 2.2.220 in /qqq-middleware-picocli
Bumps [h2](https://github.com/h2database/h2database) from 2.1.210 to 2.2.220.
- [Release notes](https://github.com/h2database/h2database/releases)
- [Commits](https://github.com/h2database/h2database/compare/version-2.1.210...version-2.2.220)

---
updated-dependencies:
- dependency-name: com.h2database:h2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-07 21:56:58 +00:00
5b84df1752 Setting loadScriptTestDetails as not-protected 2023-07-07 09:33:05 -05:00
9af1fed422 Initial backend work to support datetime query expressions from frontend 2023-07-06 18:53:10 -05:00
4299199947 CTLE-507 Fix api field sorting (make sure they have a label too) 2023-07-06 16:13:15 -05:00
6f578eb2f0 CTLE-507 Update to sort fields AFTER adding removed ones 2023-07-06 15:57:38 -05:00
be5b8f0869 CTLE-436: added missing null check 2023-07-06 13:42:02 -05:00
c27723e956 CTLE-436: added variant id to basepull key 2023-07-06 12:06:19 -05:00
cbff44f6a3 Merge branch 'dev' into integration/sprint-28 2023-07-05 08:54:32 -05:00
9167f356e9 Do not manage associations on records w/ errors 2023-07-05 08:53:17 -05:00
dccefb0a40 Merge pull request #25 from Kingsrook/feature/CTLE-503-optimization-weather-api-data
Feature/ctle 503 optimization weather api data
2023-07-03 15:41:59 -05:00
e24f15e2d8 Add commit from merge 2023-07-03 15:41:41 -05:00
d0a0f93933 Merge remote-tracking branch 'origin/integration/sprint-28' into feature/CTLE-503-optimization-weather-api-data
# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java
#	qqq-backend-module-api/src/main/java/com/kingsrook/qqq/backend/module/api/actions/BaseAPIActionUtil.java
#	qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/QRecordApiAdapter.java
2023-07-03 15:38:55 -05:00
76d226b5e8 Update query action cache helper to respect if a table says it can't do QUERY - to do GET instead 2023-07-03 15:02:26 -05:00
2bf611b24f Change apiJsonObjectToQRecord's includePrimaryKey param to be includeNonEditableFields instead 2023-07-03 14:05:33 -05:00
56b9738f15 Add convertJavaObject to QCodeExecutor 2023-07-03 14:05:07 -05:00
40d073bf54 Update to put the generated id on newly inserted cached records 2023-07-03 11:09:01 -05:00
c0297dea91 Merge branch 'feature/CTLE-436-move-to-integration-per-client' into integration/sprint-28 2023-07-03 09:57:26 -05:00
9e4743e8d8 Upgrade javalin to 5.6.1 2023-06-30 20:07:02 -05:00
976f173c93 Wrap cache table writes in try-catch - don't let a cache-updating action break a read 2023-06-30 20:06:47 -05:00
3f74594c69 Add handling of javascript Dates in convertObjectToJava 2023-06-30 20:04:49 -05:00
da08382055 Add getMaxResponseMessageLengthForLog 2023-06-30 20:00:08 -05:00
22a9e4b06b Change type to come from abstract getType method, rather than member field in base class (force sub-class to deal with it); Add ability to incldue supplemental table meta data in frontend table meta data requests 2023-06-30 14:10:23 -05:00
c086874e64 Refactor caching out of GetAction - namely, to support initial use-cases in QueryAction. 2023-06-30 12:36:15 -05:00
75ae848afd Let jsonObjectToRecord return null as a way to mean record wasn't found (therefore, don't add it to queryOutput) 2023-06-30 12:32:37 -05:00
53b74fb61d CTLE-436: attempt to add more test coverage 2023-06-29 16:03:54 -05:00
3ae938ac6e Support api-key in query-string for backend-api; 2023-06-29 11:15:24 -05:00
905ac6d72a fixed style issue 2023-06-29 10:10:00 -05:00
a6af75ebdc CTLE-436: syntax error 2023-06-29 09:38:03 -05:00
2039c727b5 CTLE-436: added variant endpoint, refactored variant code a bit 2023-06-28 19:45:37 -05:00
c38b8ac595 Fix test test and propagate exceptions more 2023-06-28 13:40:25 -05:00
3187706967 Implement RecordScriptTestInterface.execute, to fix record-script testing from UI 2023-06-28 12:39:29 -05:00
ffdb392b9f Fix handling heavy fields form joins 2023-06-28 12:39:03 -05:00
f79940d4c3 Update to clear internal caches between tests 2023-06-28 12:38:49 -05:00
be14afc11c Update to clear internal caches between tests 2023-06-28 11:17:38 -05:00
360bf56481 Add association api-meta data (so they can be versioned or excluded); add api field custom value mapper 2023-06-28 11:06:15 -05:00
b4507ba431 Remove unused withInstance method 2023-06-27 14:53:46 -05:00
57675528b5 Set query stat first result time immediately after loop (as well as inside rs loop) in case no results found (is this why we have the slow fed-ex cache use-cases?) 2023-06-27 14:53:35 -05:00
3fae35a2bf More fluent interface on core table actions & inputs 2023-06-27 14:53:01 -05:00
688d104635 Pass logs through; cleanup & comment 2023-06-27 12:33:17 -05:00
d533e59a84 Add exposed joins to QueryStat 2023-06-27 12:25:54 -05:00
184ef8db47 Mark as serializable 2023-06-27 12:25:21 -05:00
a62a1f10cd Make useOrWrap null input give null output 2023-06-27 12:25:10 -05:00
a056c4618c Fixed test (was query for contents, where they are no longer stored) 2023-06-27 08:53:20 -05:00
ed22ab5917 More test coverage on new script processes 2023-06-27 08:45:46 -05:00
598e26d9a1 Updating to support multi-file scripts 2023-06-27 08:14:11 -05:00
b53d1823df Switch to store all script contents in scriptRevisionFile sub-table; make test interface for all scripts work the same 2023-06-26 20:18:57 -05:00
6bc543fff7 Merge branch 'feature/query-stats' into feature/CTLE-507-custom-ct-live-packing-slips 2023-06-26 12:09:52 -05:00
e28f8b8317 Add null check around context 2023-06-23 16:42:47 -05:00
9d1266036c Adding scriptType fileModes, with multi-files under a script Revision. 2023-06-23 16:38:00 -05:00
b93032aae4 Merge remote-tracking branch 'origin/feature/query-stats' into feature/query-stats 2023-06-22 19:14:11 -05:00
300af89687 change 'fine grained' to have a dash 2023-06-22 19:13:55 -05:00
178 changed files with 10300 additions and 2056 deletions

View File

@ -10,7 +10,7 @@ The bundle contains all of the sub-jars. It is named:
```qqq-${version}.jar```
You can also use fine grained jars:
You can also use fine-grained jars:
- `qqq-backend-core`: The core module. Useful if you're developing other modules.
- `qqq-backend-module-rdbms`: Backend module for working with Relational Databases.
- `qqq-backend-module-filesystem`: Backend module for working with Filesystems (including AWS S3).
@ -35,4 +35,3 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -44,7 +44,7 @@
</modules>
<properties>
<revision>0.16.0-SNAPSHOT</revision>
<revision>0.18.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.actions.async;
import java.io.Serializable;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -142,6 +143,11 @@ public class AsyncRecordPipeLoop
jobState = asyncJobStatus.getState();
}
if(recordPipe instanceof BufferedRecordPipe bufferedRecordPipe)
{
bufferedRecordPipe.finalFlush();
}
LOG.debug("Job [" + jobUUID + "][" + jobName + "] completed with status: " + asyncJobStatus);
///////////////////////////////////

View File

@ -42,11 +42,11 @@ 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.audits.AuditsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
@ -178,15 +178,14 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
////////////////////////////////////////////////
// map names to ids and handle default values //
////////////////////////////////////////////////
Integer auditTableId = getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, auditSingleInput.getAuditTableName());
Integer auditUserId = getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_USER, Objects.requireNonNullElse(auditSingleInput.getAuditUserName(), getSessionUserName()));
Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(auditSingleInput.getAuditUserName(), getSessionUserName()));
Instant timestamp = Objects.requireNonNullElse(auditSingleInput.getTimestamp(), Instant.now());
//////////////////
// build record //
//////////////////
QRecord record = new QRecord()
.withValue("auditTableId", auditTableId)
.withValue("tableId", QQQTableAccessor.getTableId(auditSingleInput.getAuditTableName()))
.withValue("auditUserId", auditUserId)
.withValue("timestamp", timestamp)
.withValue("message", auditSingleInput.getMessage())
@ -268,7 +267,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
**
*******************************************************************************/
Integer getIdForName(String tableName, String nameValue) throws QException
private Integer getIdForName(String tableName, String nameValue) throws QException
{
Pair<String, String> key = new Pair<>(tableName, nameValue);
if(!cachedFetches.containsKey(key))
@ -288,15 +287,6 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
insertInput.setTableName(tableName);
QRecord record = new QRecord().withValue("name", nameValue);
if(tableName.equals(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE))
{
QTableMetaData table = QContext.getQInstance().getTable(nameValue);
if(table != null)
{
record.setValue("label", table.getLabel());
}
}
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
id = insertOutput.getRecords().get(0).getValueInteger("id");

View File

@ -33,7 +33,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -47,9 +46,7 @@ import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
@ -92,7 +89,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
List<QRecord> recordList = CollectionUtils.nonNullList(input.getRecordList()).stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors())).toList();
AuditLevel auditLevel = getAuditLevel(table);
AuditLevel auditLevel = getAuditLevel(tableActionInput);
if(auditLevel == null || auditLevel.equals(AuditLevel.NONE) || CollectionUtils.nullSafeIsEmpty(recordList))
{
/////////////////////////////////////////////
@ -197,7 +194,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
continue;
}
if(field.getType().equals(QFieldType.BLOB))
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
@ -212,7 +209,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
{
if(!Objects.equals(oldValue, value))
{
if(field.getType().equals(QFieldType.BLOB))
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
{
if(oldValue == null)
{
@ -272,54 +269,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
// new AuditAction().executeAsync(auditInput); // todo async??? maybe get that from rules???
AuditAction auditAction = new AuditAction();
auditAction.execute(auditInput);
if(DMLType.INSERT.equals(dmlType))
{
if(getIsAuditTreeRoot(table))
{
/* not needed? */
/*
Integer auditTableId = auditAction.getIdForName("auditTable", table.getName());
List<QRecord> auditTrees = recordList.stream().map(r ->
new QRecord()
.withValue("rootAuditTableId", auditTableId)
.withValue("rootRecordId", r.getValueInteger(table.getPrimaryKeyField()))
).toList();
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit tree"); // todo - from entity
insertInput.setRecords(auditTrees);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
*/
}
for(String auditTreeParentTableName : CollectionUtils.nonNullList(getAuditTreeParentTableNames(table)))
{
Integer rootAuditTableId = auditAction.getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, auditTreeParentTableName);
Integer nodeAuditTableId = auditAction.getIdForName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE, table.getName());
List<QRecord> auditTreeNodes = new ArrayList<>();
for(QRecord record : recordList)
{
Serializable rootRecordId = record.getValue("orderId"); // todo - figure this out, from joins...
auditTreeNodes.add(new QRecord()
.withValue("rootAuditTableId", rootAuditTableId)
.withValue("rootRecordId", rootRecordId)
.withValue("nodeAuditTableId", nodeAuditTableId)
.withValue("nodeRecordId", record.getValue(table.getPrimaryKeyField()))
);
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TREE);
insertInput.setRecords(auditTreeNodes);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
}
}
new AuditAction().execute(auditInput);
long end = System.currentTimeMillis();
LOG.trace("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start)));
}
@ -440,8 +390,9 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
public static AuditLevel getAuditLevel(QTableMetaData table)
public static AuditLevel getAuditLevel(AbstractTableActionInput tableActionInput)
{
QTableMetaData table = tableActionInput.getTable();
if(table.getAuditRules() == null)
{
return (AuditLevel.NONE);
@ -452,36 +403,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
public static boolean getIsAuditTreeRoot(QTableMetaData table)
{
if(table.getAuditRules() == null)
{
return (false);
}
return (table.getAuditRules().getIsAuditTreeRoot());
}
/*******************************************************************************
**
*******************************************************************************/
public static List<String> getAuditTreeParentTableNames(QTableMetaData table)
{
if(table.getAuditRules() == null)
{
return (null);
}
return (table.getAuditRules().getAuditTreeParentTableNames());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAut
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.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
@ -171,7 +172,7 @@ public class RecordAutomationStatusUpdater
CountInput countInput = new CountInput();
countInput.setTableName(TableTrigger.TABLE_NAME);
countInput.setFilter(new QQueryFilter(
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, table.getName()),
new QFilterCriteria("tableId", QCriteriaOperator.EQUALS, QQQTableAccessor.getTableId(table.getName())),
new QFilterCriteria(triggerEvent.equals(TriggerEvent.POST_INSERT) ? "postInsert" : "postUpdate", QCriteriaOperator.EQUALS, true)
));
CountOutput countOutput = new CountAction().execute(countInput);

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -74,7 +75,7 @@ public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevision.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, scriptId)));
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin("currentScriptRevision")));
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{

View File

@ -66,6 +66,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAuto
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -270,7 +271,7 @@ public class PollingAutomationPerTableRunner implements Runnable
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TableTrigger.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, table.getName()),
new QFilterCriteria("tableId", QCriteriaOperator.EQUALS, QQQTableAccessor.getTableId(table.getName())),
new QFilterCriteria(triggerEvent.equals(TriggerEvent.POST_INSERT) ? "postInsert" : "postUpdate", QCriteriaOperator.EQUALS, true)
));
QueryOutput queryOutput = new QueryAction().execute(queryInput);

View File

@ -29,6 +29,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
@ -42,18 +43,16 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Standard/re-usable post-insert customizer, for the use case where, when we
** do an insert into table "parent", we want a record automatically inserted into
** table "child", and there's a foreign key in "parent", pointed at "child"
** e.g., named: "parent.childId".
** table "child". Optionally (based on RelationshipType), there can be a foreign
** key in "parent", pointed at "child". e.g., named: "parent.childId".
**
** A similar use-case would have the foreign key in the child table - in which case,
** we could add a "Type" enum, plus abstract method to get our "Type", then logic
** to switch behavior based on type. See existing type enum, but w/ only 1 case :)
*******************************************************************************/
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
{
public enum RelationshipType
{
PARENT_POINTS_AT_CHILD
PARENT_POINTS_AT_CHILD,
CHILD_POINTS_AT_PARENT
}
@ -68,10 +67,17 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
*******************************************************************************/
public abstract String getChildTableName();
/*******************************************************************************
**
*******************************************************************************/
public abstract String getForeignKeyFieldName();
public String getForeignKeyFieldName()
{
return (null);
}
/*******************************************************************************
**
@ -88,7 +94,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
{
try
{
List<QRecord> rs = new ArrayList<>();
List<QRecord> rs = records;
List<QRecord> childrenToInsert = new ArrayList<>();
QTableMetaData table = getInsertInput().getTable();
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
@ -97,12 +103,37 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
// iterate over the inserted records, building a list child records to insert //
// for ones missing a value in the foreign key field. //
////////////////////////////////////////////////////////////////////////////////
for(QRecord record : records)
switch(getRelationshipType())
{
if(record.getValue(getForeignKeyFieldName()) == null)
case PARENT_POINTS_AT_CHILD ->
{
childrenToInsert.add(buildChildForRecord(record));
String foreignKeyFieldName = getForeignKeyFieldName();
try
{
table.getField(foreignKeyFieldName);
}
catch(Exception e)
{
throw new QRuntimeException("For RelationshipType.PARENT_POINTS_AT_CHILD, a valid foreignKeyFieldName in the parent table must be given. "
+ "[" + foreignKeyFieldName + "] is not a valid field name in table [" + table.getName() + "]");
}
for(QRecord record : records)
{
if(record.getValue(foreignKeyFieldName) == null)
{
childrenToInsert.add(buildChildForRecord(record));
}
}
}
case CHILD_POINTS_AT_PARENT ->
{
for(QRecord record : records)
{
childrenToInsert.add(buildChildForRecord(record));
}
}
default -> throw new IllegalStateException("Unexpected value: " + getRelationshipType());
}
///////////////////////////////////////////////////////////////////////////////////
@ -129,51 +160,70 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
/////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// for the PARENT_POINTS_AT_CHILD relationship type:
// iterate over the original list of records again - for any that need a child (e.g., are missing //
// foreign key), set their foreign key to a newly inserted child's key, and add them to be updated. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : records)
switch(getRelationshipType())
{
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(record.getValue(getForeignKeyFieldName()) == null)
case PARENT_POINTS_AT_CHILD ->
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// get the corresponding child record, if it has any errors, set that as a warning in the parent //
///////////////////////////////////////////////////////////////////////////////////////////////////
QRecord childRecord = insertedRecordIterator.next();
if(CollectionUtils.nullSafeHasContents(childRecord.getErrors()))
rs = new ArrayList<>();
List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : records)
{
for(QStatusMessage error : childRecord.getErrors())
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(record.getValue(getForeignKeyFieldName()) == null)
{
record.addWarning(new QWarningMessage("Error creating child " + childTable.getLabel() + " (" + error.toString() + ")"));
///////////////////////////////////////////////////////////////////////////////////////////////////
// get the corresponding child record, if it has any errors, set that as a warning in the parent //
///////////////////////////////////////////////////////////////////////////////////////////////////
QRecord childRecord = insertedRecordIterator.next();
if(CollectionUtils.nullSafeHasContents(childRecord.getErrors()))
{
for(QStatusMessage error : childRecord.getErrors())
{
record.addWarning(new QWarningMessage("Error creating child " + childTable.getLabel() + " (" + error.toString() + ")"));
}
rs.add(record);
continue;
}
Serializable foreignKey = childRecord.getValue(childTable.getPrimaryKeyField());
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
record.setValue(getForeignKeyFieldName(), foreignKey);
rs.add(record);
}
else
{
rs.add(record);
}
rs.add(record);
continue;
}
Serializable foreignKey = childRecord.getValue(childTable.getPrimaryKeyField());
recordsToUpdate.add(new QRecord().withValue(table.getPrimaryKeyField(), primaryKey).withValue(getForeignKeyFieldName(), foreignKey));
record.setValue(getForeignKeyFieldName(), foreignKey);
rs.add(record);
////////////////////////////////////////////////////////////////////////////
// update the originally inserted records to reference their new children //
////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInsertInput().getTableName());
updateInput.setRecords(recordsToUpdate);
updateInput.setTransaction(this.insertInput.getTransaction());
new UpdateAction().execute(updateInput);
}
else
case CHILD_POINTS_AT_PARENT ->
{
rs.add(record);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - some version of looking at the inserted children to confirm that they were inserted, and updating the parents with warnings if they weren't //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
default -> throw new IllegalStateException("Unexpected value: " + getRelationshipType());
}
////////////////////////////////////////////////////////////////////////////
// update the originally inserted records to reference their new children //
////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInsertInput().getTableName());
updateInput.setRecords(recordsToUpdate);
updateInput.setTransaction(this.insertInput.getTransaction());
new UpdateAction().execute(updateInput);
return (rs);
}
catch(RuntimeException re)
{
throw (re);
}
catch(Exception e)
{
throw new RuntimeException("Error inserting new child records for new parent records", e);

View File

@ -67,4 +67,15 @@ public interface BaseQueryInterface
}
}
/*******************************************************************************
**
*******************************************************************************/
default void cancelAction()
{
//////////////////////////////////////////////
// initially at least, a noop in base class //
//////////////////////////////////////////////
}
}

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRend
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
@ -50,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.NoCodeWidgetFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
@ -57,6 +59,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
@ -483,6 +486,35 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
protected String determineBasepullKeyValue(QProcessMetaData process, BasepullConfiguration basepullConfiguration) throws QException
{
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if backend specifies that it uses variants, look for that data in the session and append to our basepull key //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(process.getSchedule() != null && process.getSchedule().getVariantBackend() != null)
{
QSession session = QContext.getQSession();
QBackendMetaData backendMetaData = QContext.getQInstance().getBackend(process.getSchedule().getVariantBackend());
if(session.getBackendVariants() == null || !session.getBackendVariants().containsKey(backendMetaData.getVariantOptionsTableTypeValue()))
{
LOG.info("Could not find Backend Variant information for Backend '" + backendMetaData.getName() + "'");
}
else
{
basepullKeyValue += "-" + session.getBackendVariants().get(backendMetaData.getVariantOptionsTableTypeValue());
}
}
return (basepullKeyValue);
}
/*******************************************************************************
** Insert or update the last runtime value for this basepull into the backend.
*******************************************************************************/
@ -491,7 +523,7 @@ public class RunProcessAction
String basepullTableName = basepullConfiguration.getTableName();
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
///////////////////////////////////////
// get the stored basepull timestamp //
@ -571,7 +603,7 @@ public class RunProcessAction
String basepullKeyFieldName = basepullConfiguration.getKeyField();
String basepullLastRunTimeFieldName = basepullConfiguration.getLastRunTimeFieldName();
Integer basepullHoursBackForInitialTimestamp = basepullConfiguration.getHoursBackForInitialTimestamp();
String basepullKeyValue = (basepullConfiguration.getKeyValue() != null) ? basepullConfiguration.getKeyValue() : process.getName();
String basepullKeyValue = determineBasepullKeyValue(process, basepullConfiguration);
///////////////////////////////////////
// get the stored basepull timestamp //

View File

@ -23,22 +23,33 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -133,7 +144,17 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision)
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision) throws QException
{
return setupExecuteCodeInput(input, scriptRevision, null);
}
/*******************************************************************************
**
*******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision, String fileName) throws QException
{
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
@ -150,7 +171,49 @@ public class ExecuteCodeAction
context.put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
if(CollectionUtils.nullSafeIsEmpty(scriptRevision.getFiles()))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevisionFile.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, scriptRevision.getId())));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
scriptRevision.setFiles(new ArrayList<>());
for(QRecord record : queryOutput.getRecords())
{
scriptRevision.getFiles().add(new ScriptRevisionFile(record));
}
}
List<ScriptRevisionFile> files = scriptRevision.getFiles();
if(files == null || files.isEmpty())
{
throw (new QException("Script Revision " + scriptRevision.getId() + " had more than 1 associated ScriptRevisionFile (and the name to use was not specified)."));
}
else
{
String contents = null;
if(fileName == null || files.size() == 1)
{
contents = files.get(0).getContents();
}
else
{
for(ScriptRevisionFile file : files)
{
if(file.getFileName().equals(fileName))
{
contents = file.getContents();
}
}
if(contents == null)
{
throw (new QException("Could not find file named " + fileName + " for Script Revision " + scriptRevision.getId()));
}
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(contents).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
}
ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision);
context.put("qqq", new QqqScriptUtils());
@ -168,7 +231,19 @@ public class ExecuteCodeAction
*******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, ScriptRevision scriptRevision)
{
if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion()))
addApiUtilityToContext(context, scriptRevision.getApiName(), scriptRevision.getApiVersion());
}
/*******************************************************************************
** Try to (dynamically) load the ApiScriptUtils object from the api middleware
** module -- in case the runtime doesn't have that module deployed (e.g, not in
** the project pom).
*******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, String apiName, String apiVersion)
{
if(!StringUtils.hasContent(apiName) || !StringUtils.hasContent(apiVersion))
{
return;
}
@ -176,7 +251,7 @@ public class ExecuteCodeAction
try
{
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(scriptRevision.getApiName(), scriptRevision.getApiVersion());
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(apiName, apiVersion);
context.put("api", (Serializable) apiScriptUtilsObject);
}
catch(ClassNotFoundException e)

View File

@ -46,7 +46,17 @@ public interface QCodeExecutor
** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such
**
*******************************************************************************/
default Object convertObjectToJava(Object object)
default Object convertObjectToJava(Object object) throws QCodeException
{
return (object);
}
/*******************************************************************************
** Convert a native java object into one for the script's language/runtime.
** e.g., a java Instant to a Nashorn Date
**
*******************************************************************************/
default Object convertJavaObject(Object object, Object requestedTypeHint) throws QCodeException
{
return (object);
}

View File

@ -0,0 +1,160 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class RecordScriptTestInterface implements TestScriptActionInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput) throws QException
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(TestScriptInput input, TestScriptOutput output) throws QException
{
try
{
Serializable scriptId = input.getInputValues().get("scriptId");
QRecord script = new GetAction().executeForRecord(new GetInput(Script.TABLE_NAME).withPrimaryKey(scriptId));
//////////////////////////////////////////////
// look up the records being tested against //
//////////////////////////////////////////////
String tableName = QQQTableAccessor.getTableName(script.getValueInteger("tableId"));
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
throw (new QException("Could not find table [" + tableName + "] for script"));
}
String recordPrimaryKeyList = ValueUtils.getValueAsString(input.getInputValues().get("recordPrimaryKeyList"));
if(!StringUtils.hasContent(recordPrimaryKeyList))
{
throw (new QException("Record primary key list was not given."));
}
QueryOutput queryOutput = new QueryAction().execute(new QueryInput(tableName)
.withFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))))
.withIncludeAssociations(true));
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{
throw (new QException("No records were found by the given primary keys."));
}
/////////////////////////////
// set up & run the action //
/////////////////////////////
RunAdHocRecordScriptInput runAdHocRecordScriptInput = new RunAdHocRecordScriptInput();
runAdHocRecordScriptInput.setRecordList(queryOutput.getRecords());
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
runAdHocRecordScriptInput.setLogger(executionLogger);
runAdHocRecordScriptInput.setTableName(tableName);
runAdHocRecordScriptInput.setCodeReference((AdHocScriptCodeReference) input.getCodeReference());
RunAdHocRecordScriptOutput runAdHocRecordScriptOutput = new RunAdHocRecordScriptOutput();
new RunAdHocRecordScriptAction().run(runAdHocRecordScriptInput, runAdHocRecordScriptOutput);
/////////////////////////////////
// send outputs back to caller //
/////////////////////////////////
output.setScriptLog(executionLogger.getScriptLog());
output.setScriptLogLines(executionLogger.getScriptLogLines());
if(runAdHocRecordScriptOutput.getException().isPresent())
{
output.setException(runAdHocRecordScriptOutput.getException().get());
}
}
catch(QException e)
{
output.setException(e);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QFieldMetaData> getTestInputFields()
{
return (List.of(new QFieldMetaData("recordPrimaryKeyList", QFieldType.STRING).withLabel("Record Primary Key List")));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QFieldMetaData> getTestOutputFields()
{
return (Collections.emptyList());
}
}

View File

@ -51,6 +51,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReferen
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -197,7 +198,7 @@ public class RunAdHocRecordScriptAction
QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevision.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("script.id", QCriteriaOperator.EQUALS, codeReference.getScriptId())));
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin("currentScriptRevision")));
queryInput.withQueryJoin(new QueryJoin(Script.TABLE_NAME).withBaseTableOrAlias(ScriptRevision.TABLE_NAME).withJoinMetaData(QContext.getQInstance().getJoin(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))

View File

@ -110,6 +110,7 @@ public class RunAssociatedScriptAction
GetInput getInput = new GetInput();
getInput.setTableName("scriptRevision");
getInput.setPrimaryKey(scriptRevisionId);
getInput.setIncludeAssociations(true);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{

View File

@ -31,6 +31,8 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
@ -47,6 +49,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.StoreScriptRevisionProcessStep;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -180,41 +183,19 @@ public class StoreAssociatedScriptAction
}
}
QRecord scriptRevision = new QRecord()
.withValue("scriptId", script.getValue("id"))
.withValue("contents", input.getCode())
.withValue("apiName", input.getApiName())
.withValue("apiVersion", input.getApiVersion())
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);
try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);
RunBackendStepInput storeScriptRevisionInput = new RunBackendStepInput();
storeScriptRevisionInput.addValue("scriptId", script.getValue("id"));
storeScriptRevisionInput.addValue("commitMessage", commitMessage);
storeScriptRevisionInput.addValue("apiName", input.getApiName());
storeScriptRevisionInput.addValue("apiVersion", input.getApiVersion());
storeScriptRevisionInput.addValue("fileNames", "script");
storeScriptRevisionInput.addValue("fileContents:script", input.getCode());
RunBackendStepOutput storeScriptRevisionOutput = new RunBackendStepOutput();
new StoreScriptRevisionProcessStep().run(storeScriptRevisionInput, storeScriptRevisionOutput);
output.setScriptId(script.getValueInteger("id"));
output.setScriptName(script.getValueString("name"));
output.setScriptRevisionId(scriptRevision.getValueInteger("id"));
output.setScriptRevisionSequenceNo(scriptRevision.getValueInteger("sequenceNo"));
output.setScriptRevisionId(storeScriptRevisionOutput.getValueInteger("scriptRevisionId"));
output.setScriptRevisionSequenceNo(storeScriptRevisionOutput.getValueInteger("scriptRevisionSequenceNo"));
}
}

View File

@ -41,6 +41,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
private QCodeReference qCodeReference;
private String uuid = UUID.randomUUID().toString();
private boolean includeUUID = true;
/*******************************************************************************
@ -52,7 +53,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
this.qCodeReference = executeCodeInput.getCodeReference();
String inputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(executeCodeInput.getInput()), 250, "...");
LOG.info("Starting script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with input: " + inputString);
LOG.info("Starting script execution: " + qCodeReference.getName() + (includeUUID ? ", uuid: " + uuid : "") + ", with input: " + inputString);
}
@ -63,7 +64,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
@Override
public void acceptLogLine(String logLine)
{
LOG.info("Script log: " + uuid + ": " + logLine);
LOG.info("Script log: " + (includeUUID ? uuid + ": " : "") + logLine);
}
@ -74,7 +75,7 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
@Override
public void acceptException(Exception exception)
{
LOG.info("Script Exception: " + uuid, exception);
LOG.info("Script Exception: " + (includeUUID ? uuid : ""), exception);
}
@ -86,7 +87,38 @@ public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
public void acceptExecutionEnd(Serializable output)
{
String outputString = StringUtils.safeTruncate(ValueUtils.getValueAsString(output), 250, "...");
LOG.info("Finished script execution: " + qCodeReference.getName() + ", uuid: " + uuid + ", with output: " + outputString);
LOG.info("Finished script execution: " + qCodeReference.getName() + (includeUUID ? ", uuid: " + uuid : "") + ", with output: " + outputString);
}
/*******************************************************************************
** Getter for includeUUID
*******************************************************************************/
public boolean getIncludeUUID()
{
return (this.includeUUID);
}
/*******************************************************************************
** Setter for includeUUID
*******************************************************************************/
public void setIncludeUUID(boolean includeUUID)
{
this.includeUUID = includeUUID;
}
/*******************************************************************************
** Fluent setter for includeUUID
*******************************************************************************/
public Log4jCodeExecutionLogger withIncludeUUID(boolean includeUUID)
{
this.includeUUID = includeUUID;
return (this);
}
}

View File

@ -29,7 +29,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
/*******************************************************************************
** Interface to provide logging functionality to QCodeExecution (e.g., scripts)
*******************************************************************************/
public interface QCodeExecutionLoggerInterface
public interface QCodeExecutionLoggerInterface extends Serializable
{
/*******************************************************************************

View File

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

View File

@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
@ -41,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class AggregateAction
{
private static final QLogger LOG = QLogger.getLogger(AggregateAction.class);
private AggregateInterface aggregateInterface;
/*******************************************************************************
**
*******************************************************************************/
@ -56,7 +63,7 @@ public class AggregateAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
AggregateInterface aggregateInterface = qModule.getAggregateInterface();
aggregateInterface = qModule.getAggregateInterface();
aggregateInterface.setQueryStat(queryStat);
AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput);
@ -64,4 +71,20 @@ public class AggregateAction
return aggregateOutput;
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(aggregateInterface == null)
{
LOG.warn("aggregateInterface object was null when requested to cancel");
return;
}
aggregateInterface.cancelAction();
}
}

View File

@ -26,6 +26,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
@ -41,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class CountAction
{
private static final QLogger LOG = QLogger.getLogger(CountAction.class);
private CountInterface countInterface;
/*******************************************************************************
**
*******************************************************************************/
@ -56,7 +63,7 @@ public class CountAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
CountInterface countInterface = qModule.getCountInterface();
countInterface = qModule.getCountInterface();
countInterface.setQueryStat(queryStat);
CountOutput countOutput = countInterface.execute(countInput);
@ -64,4 +71,20 @@ public class CountAction
return countOutput;
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(countInterface == null)
{
LOG.warn("countInterface object was null when requested to cancel");
return;
}
countInterface.cancelAction();
}
}

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
@ -117,12 +118,14 @@ public class DeleteAction
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a query filter, but the interface doesn't support using a query filter, then do a query for the filter, to get a list of primary keys instead //
// or - anytime there are associations on the table we want primary keys, as that's what the manage associations method uses //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
if(deleteInput.getQueryFilter() != null && (!deleteInterface.supportsQueryFilterInput() || CollectionUtils.nullSafeHasContents(table.getAssociations())))
{
LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes");
LOG.info("Querying for primary keys, for table " + table.getName() + " in backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes (or the table has associations)");
List<Serializable> primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
deleteInput.setPrimaryKeys(primaryKeyList);
primaryKeys = primaryKeyList;
if(primaryKeyList.isEmpty())
{
@ -165,10 +168,22 @@ public class DeleteAction
if(!primaryKeysToRemoveFromInput.isEmpty())
{
primaryKeys.removeAll(primaryKeysToRemoveFromInput);
if(primaryKeys == null)
{
LOG.warn("There were primary keys to remove from the input, but no primary key list (filter supplied as input?)", new LogPair("primaryKeysToRemoveFromInput", primaryKeysToRemoveFromInput));
}
else
{
primaryKeys.removeAll(primaryKeysToRemoveFromInput);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// stash a copy of primary keys that didn't have errors (for use in manageAssociations below) //
////////////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> primaryKeysWithoutErrors = new HashSet<>(CollectionUtils.nonNullList(primaryKeys));
////////////////////////////////////
// have the backend do the delete //
////////////////////////////////////
@ -187,11 +202,13 @@ public class DeleteAction
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if a record had a validation warning, but then an execution error, remove it from the warning list - so it's only in one of them. //
// also, always remove from
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord outputRecordWithError : outputRecordsWithErrors)
{
Serializable pkey = outputRecordWithError.getValue(primaryKeyFieldName);
recordsWithValidationWarnings.remove(pkey);
primaryKeysWithoutErrors.remove(pkey);
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -211,15 +228,23 @@ public class DeleteAction
////////////////////////////////////////
// delete associations, if applicable //
////////////////////////////////////////
manageAssociations(deleteInput);
manageAssociations(primaryKeysWithoutErrors, deleteInput);
///////////////////////////////////
// do the audit //
// todo - add input.omitDmlAudit //
///////////////////////////////////
DMLAuditInput dmlAuditInput = new DMLAuditInput().withTableActionInput(deleteInput);
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
new DMLAuditAction().execute(dmlAuditInput);
//////////////////
// do the audit //
//////////////////
if(deleteInput.getOmitDmlAudit())
{
LOG.debug("Requested to omit DML audit");
}
else
{
DMLAuditInput dmlAuditInput = new DMLAuditInput()
.withTableActionInput(deleteInput)
.withAuditContext(deleteInput.getAuditContext());
oldRecordList.ifPresent(l -> dmlAuditInput.setRecordList(l));
new DMLAuditAction().execute(dmlAuditInput);
}
//////////////////////////////////////////////////////////////
// finally, run the post-delete customizer, if there is one //
@ -340,7 +365,7 @@ public class DeleteAction
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(DeleteInput deleteInput) throws QException
private void manageAssociations(Set<Serializable> primaryKeysWithoutErrors, DeleteInput deleteInput) throws QException
{
QTableMetaData table = deleteInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
@ -353,7 +378,7 @@ public class DeleteAction
if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField()))
{
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, deleteInput.getPrimaryKeys()));
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, new ArrayList<>(primaryKeysWithoutErrors)));
}
else
{

View File

@ -23,8 +23,6 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -33,35 +31,24 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCusto
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.GetActionCacheHelper;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
@ -70,8 +57,6 @@ import org.apache.commons.lang.NotImplementedException;
*******************************************************************************/
public class GetAction
{
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
private GetInput getInput;
@ -79,6 +64,16 @@ public class GetAction
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(GetInput getInput) throws QException
{
return (execute(getInput).getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
@ -125,36 +120,7 @@ public class GetAction
////////////////////////////
if(table.getCacheOf() != null)
{
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////////////////////
// if the record wasn't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// good, we found a record from the source, make sure we should cache it, and if so, do it now //
/////////////////////////////////////////////////////////////////////////////////////////////////
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
boolean shouldCacheRecord = shouldCacheRecord(table, recordToCache);
if(shouldCacheRecord)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////
// if the record was found, but it's too old, maybe re-fetch from cache source //
/////////////////////////////////////////////////////////////////////////////////
refreshCacheIfExpired(getInput, getOutput);
}
new GetActionCacheHelper().handleCaching(getInput, getOutput);
}
////////////////////////////////////////////////////////
@ -171,168 +137,12 @@ public class GetAction
/*******************************************************************************
**
** Run a GetAction by using the QueryAction instead (e.g., with a filter made
** from the pkey/ukey, and returning the single record if found).
*******************************************************************************/
private boolean shouldCacheRecord(QTableMetaData table, QRecord recordToCache)
public GetOutput executeViaQuery(GetInput getInput) throws QException
{
boolean shouldCacheRecord = true;
recordMatchExclusionLoop:
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
{
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
{
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
shouldCacheRecord = false;
break recordMatchExclusionLoop;
}
}
}
return (shouldCacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource)
{
QRecord cacheRecord = new QRecord(recordFromSource);
//////////////////////////////////////////////////////////////////////////////////////////////
// make sure every value in the qRecord is set, because we will possibly be doing an update //
// on this record and want to null out any fields not set, not leave them populated //
//////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : table.getFields().keySet())
{
if(!cacheRecord.getValues().containsKey(fieldName))
{
cacheRecord.setValue(fieldName, null);
}
}
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
{
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
}
return (cacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
{
QTableMetaData table = getInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
QRecord cachedRecord = getOutput.getRecord();
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
//////////////////////////////////////////////////////////////////////////
// keep the serial key from the old record in case we need to delete it //
//////////////////////////////////////////////////////////////////////////
Serializable oldRecordPrimaryKey = getOutput.getRecord().getValue(table.getPrimaryKeyField());
boolean shouldDeleteCachedRecord = true;
///////////////////////////////////////////
// fetch record from original source now //
///////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
//////////////////////////////////////////////////////////////////////
// if the record was found in the source, put it into the output //
// object so returned back to caller, check that it should actually //
// be cached before doing so //
//////////////////////////////////////////////////////////////////////
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
getOutput.setRecord(recordToCache);
if(shouldCacheRecord(table, recordToCache))
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
getOutput.setRecord(updateOutput.getRecords().get(0));
shouldDeleteCachedRecord = false;
}
}
else
{
///////////////////////////////////////////////////////////////////////////////////////
// if we did not get a record back from the source, empty out the getOutput's record //
///////////////////////////////////////////////////////////////////////////////////////
getOutput.setRecord(null);
}
if(shouldDeleteCachedRecord)
{
/////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source, then remove it from the cache //
/////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(oldRecordPrimaryKey));
new DeleteAction().execute(deleteInput);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord tryToGetFromCacheSource(GetInput getInput) throws QException
{
QRecord recordFromSource = null;
QTableMetaData table = getInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
{
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
break;
}
else
{
// todo!!
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
}
}
return (recordFromSource);
}
/*******************************************************************************
**
*******************************************************************************/
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
{
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
QRecord outputRecord = sourceGetOutput.getRecord();
return (outputRecord);
return (new DefaultGetInterface().execute(getInput));
}
@ -345,42 +155,7 @@ public class GetAction
@Override
public GetOutput execute(GetInput getInput) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
// build filter using either pkey or unique key //
//////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
if(getInput.getPrimaryKey() != null)
{
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
}
else if(getInput.getUniqueKey() != null)
{
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
{
if(entry.getValue() == null)
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
}
else
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
}
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
}
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
queryInput.setShouldMaskPasswords(getInput.getShouldMaskPasswords());
queryInput.setShouldOmitHiddenFields(getInput.getShouldOmitHiddenFields());
QueryInput queryInput = convertGetInputToQueryInput(getInput);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
@ -395,6 +170,48 @@ public class GetAction
/*******************************************************************************
**
*******************************************************************************/
public static QueryInput convertGetInputToQueryInput(GetInput getInput) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
// build filter using either pkey or unique key //
//////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
if(getInput.getPrimaryKey() != null)
{
filter.addCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, getInput.getPrimaryKey()));
}
else if(getInput.getUniqueKey() != null)
{
for(Map.Entry<String, Serializable> entry : getInput.getUniqueKey().entrySet())
{
if(entry.getValue() == null)
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.IS_BLANK));
}
else
{
filter.addCriteria(new QFilterCriteria(entry.getKey(), QCriteriaOperator.EQUALS, entry.getValue()));
}
}
}
else
{
throw (new QException("No primaryKey or uniqueKey was passed to Get"));
}
queryInput.setFilter(filter);
queryInput.setCommonParamsFrom(getInput);
return queryInput;
}
/*******************************************************************************
** Run the necessary actions on a record. This may include setting display values,
** translating possible values, and running post-record customizations.

View File

@ -78,6 +78,28 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(InsertInput insertInput) throws QException
{
InsertOutput insertOutput = new InsertAction().execute(insertInput);
return (insertOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecord> executeForRecords(InsertInput insertInput) throws QException
{
InsertOutput insertOutput = new InsertAction().execute(insertInput);
return (insertOutput.getRecords());
}
/*******************************************************************************
**
*******************************************************************************/
@ -133,7 +155,10 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
}
else
{
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
new DMLAuditAction().execute(new DMLAuditInput()
.withTableActionInput(insertInput)
.withAuditContext(insertInput.getAuditContext())
.withRecordList(insertOutput.getRecords()));
}
//////////////////////////////////////////////////////////////

View File

@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryActionCacheHelper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
@ -75,6 +76,7 @@ public class QueryAction
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private QueryInput queryInput;
private QueryInterface queryInterface;
private QPossibleValueTranslator qPossibleValueTranslator;
@ -120,12 +122,20 @@ public class QueryAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
QueryInterface queryInterface = qModule.getQueryInterface();
queryInterface = qModule.getQueryInterface();
queryInterface.setQueryStat(queryStat);
QueryOutput queryOutput = queryInterface.execute(queryInput);
QueryStatManager.getInstance().add(queryStat);
////////////////////////////
// handle cache use-cases //
////////////////////////////
if(table.getCacheOf() != null)
{
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
}
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
{
bufferedRecordPipe.finalFlush();
@ -330,4 +340,20 @@ public class QueryAction
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(queryInterface == null)
{
LOG.warn("queryInterface object was null when requested to cancel");
return;
}
queryInterface.cancelAction();
}
}

View File

@ -0,0 +1,174 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.replace.ReplaceOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Action to do a "replace" - e.g: Update rows with unique-key values that are
** already in the table; insert rows whose unique keys weren't already in the
** table, and delete rows that weren't in the input (all based on a
** UniqueKey that's part of the input)
**
** Note - the filter in the ReplaceInput - its role is to limit what rows are
** potentially deleted. e.g., if you have a table that's segmented, and you're
** only replacing a particular segment of it (say, for 1 client), then you pass
** in a filter that finds rows matching that segment. See Test for example.
*******************************************************************************/
public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, ReplaceOutput>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public ReplaceOutput execute(ReplaceInput input) throws QException
{
ReplaceOutput output = new ReplaceOutput();
QBackendTransaction transaction = input.getTransaction();
boolean weOwnTheTransaction = false;
try
{
QTableMetaData table = input.getTable();
UniqueKey uniqueKey = input.getKey();
String primaryKeyField = table.getPrimaryKeyField();
if(transaction == null)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(input.getTableName());
transaction = new InsertAction().openTransaction(insertInput);
weOwnTheTransaction = true;
}
List<QRecord> insertList = new ArrayList<>();
List<QRecord> updateList = new ArrayList<>();
List<Serializable> primaryKeysToKeep = new ArrayList<>();
for(List<QRecord> page : CollectionUtils.getPages(input.getRecords(), 1000))
{
///////////////////////////////////////////////////////////////////////////////////
// originally it was thought that we'd need to pass the filter in here //
// but, it's been decided not to. the filter only applies to what we can delete //
///////////////////////////////////////////////////////////////////////////////////
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey);
for(QRecord record : page)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
if(keyValues.isPresent())
{
if(existingKeys.containsKey(keyValues.get()))
{
Serializable primaryKey = existingKeys.get(keyValues.get());
record.setValue(primaryKeyField, primaryKey);
updateList.add(record);
primaryKeysToKeep.add(primaryKey);
}
else
{
insertList.add(record);
}
}
}
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName(table.getName());
insertInput.setRecords(insertList);
insertInput.setTransaction(transaction);
insertInput.setOmitDmlAudit(input.getOmitDmlAudit());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
primaryKeysToKeep.addAll(insertOutput.getRecords().stream().map(r -> r.getValue(primaryKeyField)).toList());
output.setInsertOutput(insertOutput);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
updateInput.setRecords(updateList);
updateInput.setTransaction(transaction);
updateInput.setOmitDmlAudit(input.getOmitDmlAudit());
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
output.setUpdateOutput(updateOutput);
QQueryFilter deleteFilter = new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.NOT_IN, primaryKeysToKeep));
if(input.getFilter() != null)
{
deleteFilter.addSubFilter(input.getFilter());
}
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(table.getName());
deleteInput.setQueryFilter(deleteFilter);
deleteInput.setTransaction(transaction);
deleteInput.setOmitDmlAudit(input.getOmitDmlAudit());
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
output.setDeleteOutput(deleteOutput);
if(weOwnTheTransaction)
{
transaction.commit();
}
return (output);
}
catch(Exception e)
{
if(weOwnTheTransaction)
{
transaction.rollback();
}
throw (new QException("Error executing replace action", e));
}
finally
{
if(weOwnTheTransaction)
{
transaction.close();
}
}
}
}

View File

@ -83,6 +83,28 @@ public class UpdateAction
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(UpdateInput updateInput) throws QException
{
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
return (updateOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecord> executeForRecords(UpdateInput updateInput) throws QException
{
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
return (updateOutput.getRecords());
}
/*******************************************************************************
**
*******************************************************************************/
@ -374,6 +396,11 @@ public class UpdateAction
//////////////////////////////////////////////////////
for(QRecord record : page)
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
continue;
}
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -0,0 +1,109 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/*******************************************************************************
** For actions that may want to set a timeout, and cancel themselves if they run
** too long - this class helps.
**
** Construct with the timeout (delay & timeUnit), and a runnable that takes care
** of doing the cancel (e.g., cancelling a JDBC statement).
**
** Call start() to make a future get scheduled (note, if delay was null or <= 0,
** then it doesn't get scheduled at all).
**
** Call cancel() if the action got far enough/completed, to cancel the future.
**
** You can check didTimeout (getDidTimeout()) to know if the timeout did occur.
*******************************************************************************/
public class ActionTimeoutHelper
{
private final Integer delay;
private final TimeUnit timeUnit;
private final Runnable runnable;
private ScheduledFuture<?> future;
private boolean didTimeout = false;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ActionTimeoutHelper(Integer delay, TimeUnit timeUnit, Runnable runnable)
{
this.delay = delay;
this.timeUnit = timeUnit;
this.runnable = runnable;
}
/*******************************************************************************
**
*******************************************************************************/
public void start()
{
if(delay == null || delay <= 0)
{
return;
}
future = Executors.newSingleThreadScheduledExecutor().schedule(() ->
{
didTimeout = true;
runnable.run();
}, delay, timeUnit);
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(future != null)
{
future.cancel(true);
}
}
/*******************************************************************************
** Getter for didTimeout
**
*******************************************************************************/
public boolean getDidTimeout()
{
return didTimeout;
}
}

View File

@ -0,0 +1,104 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class CacheUtils
{
private static final QLogger LOG = QLogger.getLogger(CacheUtils.class);
/*******************************************************************************
**
*******************************************************************************/
static QRecord mapSourceRecordToCacheRecord(QTableMetaData table, QRecord recordFromSource, CacheUseCase cacheUseCase)
{
QRecord cacheRecord = new QRecord(recordFromSource);
//////////////////////////////////////////////////////////////////////////////////////////////
// make sure every value in the qRecord is set, because we will possibly be doing an update //
// on this record and want to null out any fields not set, not leave them populated //
//////////////////////////////////////////////////////////////////////////////////////////////
for(String fieldName : table.getFields().keySet())
{
if(fieldName.equals(table.getPrimaryKeyField()))
{
if(!cacheUseCase.getDoCopySourcePrimaryKeyToCache())
{
cacheRecord.removeValue(fieldName);
}
}
else if(!cacheRecord.getValues().containsKey(fieldName))
{
cacheRecord.setValue(fieldName, null);
}
}
if(StringUtils.hasContent(table.getCacheOf().getCachedDateFieldName()))
{
cacheRecord.setValue(table.getCacheOf().getCachedDateFieldName(), Instant.now());
}
return (cacheRecord);
}
/*******************************************************************************
**
*******************************************************************************/
static boolean shouldCacheRecord(QTableMetaData table, QRecord recordToCache)
{
boolean shouldCacheRecord = true;
recordMatchExclusionLoop:
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
{
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
{
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
shouldCacheRecord = false;
break recordMatchExclusionLoop;
}
}
}
return (shouldCacheRecord);
}
}

View File

@ -0,0 +1,241 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public class GetActionCacheHelper
{
private static final QLogger LOG = QLogger.getLogger(GetActionCacheHelper.class);
/*******************************************************************************
**
*******************************************************************************/
public void handleCaching(GetInput getInput, GetOutput getOutput) throws QException
{
///////////////////////////////////////////////////////
// copy Get input & output into Query input & output //
///////////////////////////////////////////////////////
QueryInput queryInput = GetAction.convertGetInputToQueryInput(getInput);
QueryOutput queryOutput = new QueryOutput(queryInput);
if(getOutput.getRecord() != null)
{
queryOutput.addRecord(getOutput.getRecord());
}
////////////////////////////////////
// run the QueryActionCacheHelper //
////////////////////////////////////
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
///////////////////////////////////
// set result back in get output //
///////////////////////////////////
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
getOutput.setRecord(queryOutput.getRecords().get(0));
}
else
{
getOutput.setRecord(null);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// In July 2023, initial caching was added in QueryAction. //
// at this time, it felt wrong to essentially duplicate this code between Get & Query - as Get is a simplified use-case of Query. //
// so - we'll keep this code here, as a potential quick/easy fallback - but - see above - where we use QueryActionCacheHelper instead. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
public void handleCaching(GetInput getInput, GetOutput getOutput) throws QException
{
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////////////////////
// if the record wasn't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// good, we found a record from the source, make sure we should cache it, and if so, do it now //
// note, we always return the record from the source, even if we don't cache it. //
/////////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = getInput.getTable();
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, recordFromSource);
getOutput.setRecord(recordToCache);
boolean shouldCacheRecord = CacheUtils.shouldCacheRecord(table, recordToCache);
if(shouldCacheRecord)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
/////////////////////////////////////////////////////////////////////////////////////////////
// update the result record from the insert (e.g., so we get its id, just in case we care) //
/////////////////////////////////////////////////////////////////////////////////////////////
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////
// if the record was found, but it's too old, maybe re-fetch from cache source //
/////////////////////////////////////////////////////////////////////////////////
refreshCacheIfExpired(getInput, getOutput);
}
}
private QRecord tryToGetFromCacheSource(GetInput getInput) throws QException
{
QRecord recordFromSource = null;
QTableMetaData table = getInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(cacheUseCase.getType()) && getInput.getUniqueKey() != null)
{
recordFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(getInput, table.getCacheOf().getSourceTable());
break;
}
else
{
// todo!!
throw new NotImplementedException("Not-yet-implemented cache use case type: " + cacheUseCase.getType());
}
}
return (recordFromSource);
}
private void refreshCacheIfExpired(GetInput getInput, GetOutput getOutput) throws QException
{
QTableMetaData table = getInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
QRecord cachedRecord = getOutput.getRecord();
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
//////////////////////////////////////////////////////////////////////////
// keep the serial key from the old record in case we need to delete it //
//////////////////////////////////////////////////////////////////////////
Serializable oldRecordPrimaryKey = cachedRecord.getValue(table.getPrimaryKeyField());
boolean shouldDeleteCachedRecord;
///////////////////////////////////////////
// fetch record from original source now //
///////////////////////////////////////////
QRecord recordFromSource = tryToGetFromCacheSource(getInput);
if(recordFromSource != null)
{
///////////////////////////////////////////////////////////////////
// if the record was found in the source, put it into the output //
// object so returned back to caller //
///////////////////////////////////////////////////////////////////
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
getOutput.setRecord(recordToCache);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the record should be cached, update the cache record - else set the flag to delete the cached record. //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(CacheUtils.shouldCacheRecord(table, recordToCache))
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
getOutput.setRecord(updateOutput.getRecords().get(0));
shouldDeleteCachedRecord = false;
}
else
{
shouldDeleteCachedRecord = true;
}
}
else
{
///////////////////////////////////////////////////////////////////////////////////////
// if we did not get a record back from the source, empty out the getOutput's record //
// and set the flag to delete the cached record //
///////////////////////////////////////////////////////////////////////////////////////
getOutput.setRecord(null);
shouldDeleteCachedRecord = true;
}
if(shouldDeleteCachedRecord)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source (or it was in the source, but failed the should-cache check), then remove it from the cache //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(oldRecordPrimaryKey));
new DeleteAction().execute(deleteInput);
}
}
}
}
private QRecord getFromCachedSourceForUniqueKeyToUniqueKey(GetInput getInput, String sourceTableName) throws QException
{
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
QRecord outputRecord = sourceGetOutput.getRecord();
return (outputRecord);
}
*/
}

View File

@ -0,0 +1,622 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** After running a query, if it's for a table that's a CacheOf another table,
** see if there are any cache use-cases to apply to the query result.
**
** Such as:
** - if it's a query for one or more values in a UniqueKey:
** - if any particular UniqueKeys weren't found, look in the source table
** - if any cached records are expired, refresh them from the source
** - possibly updating the cached record; possibly deleting it.
*******************************************************************************/
public class QueryActionCacheHelper
{
private static final QLogger LOG = QLogger.getLogger(QueryActionCacheHelper.class);
private boolean isQueryInputCacheable = false;
private Map<CacheUseCase.Type, CacheUseCase> cacheUseCaseMap = new HashMap<>();
private CacheUseCase activeCacheUseCase = null;
private UniqueKey cacheUniqueKey = null;
private ListingHash<String, Serializable> uniqueKeyValues = new ListingHash<>();
/*******************************************************************************
**
*******************************************************************************/
public void handleCaching(QueryInput queryInput, QueryOutput queryOutput) throws QException
{
analyzeInput(queryInput);
if(!isQueryInputCacheable)
{
return;
}
//////////////////////////////////////////////////////////////////////////
// figure out which keys in the query were found, and which were missed //
//////////////////////////////////////////////////////////////////////////
List<QRecord> recordsFoundInCache = new ArrayList<>(queryOutput.getRecords());
Set<List<Serializable>> uniqueKeyValuesInFoundRecords = getUniqueKeyValuesFromFoundRecords(queryOutput.getRecords());
Set<List<Serializable>> missedUniqueKeyValues = getUniqueKeyValuesFromQuery();
missedUniqueKeyValues.removeAll(uniqueKeyValuesInFoundRecords);
///////////////////////////////////////////////////////////////////////////////////
// if any requested records weren't found, see if we should look in cache-source //
///////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(missedUniqueKeyValues))
{
List<QRecord> recordsFromSource = tryToGetFromCacheSource(queryInput, missedUniqueKeyValues);
if(CollectionUtils.nullSafeHasContents(recordsFromSource))
{
//////////////////////////////////////////////////////////////////////////////////////////////////
// good, we found records from the source, make sure we should cache them, and if so, do it now //
// note, we always return the record from the source, even if we don't cache it. //
//////////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = queryInput.getTable();
List<QRecord> recordsToReturn = recordsFromSource.stream()
.map(r -> CacheUtils.mapSourceRecordToCacheRecord(table, r, activeCacheUseCase))
.toList();
queryOutput.addRecords(recordsToReturn);
List<QRecord> recordsToCache = recordsToReturn.stream()
.filter(r -> CacheUtils.shouldCacheRecord(table, r))
.toList();
if(CollectionUtils.nullSafeHasContents(recordsToCache))
{
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(queryInput.getTableName());
insertInput.setRecords(recordsToCache);
insertInput.setSkipUniqueKeyCheck(true);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
//////////////////////////////////////////////////////////
// set the (generated) ids in the records being returne //
//////////////////////////////////////////////////////////
Map<List<Serializable>, QRecord> insertedRecordsByUniqueKey = new HashMap<>();
for(QRecord record : insertOutput.getRecords())
{
insertedRecordsByUniqueKey.put(getUniqueKeyValues(record), record);
}
for(QRecord record : recordsToReturn)
{
QRecord insertedRecord = insertedRecordsByUniqueKey.get(getUniqueKeyValues(record));
if(insertedRecord != null)
{
record.setValue(table.getPrimaryKeyField(), insertedRecord.getValue(table.getPrimaryKeyField()));
}
}
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't let an exception break this query - it (probably) just indicates some data that didn't get cached - so - that's generally "ok" //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error inserting cached records", e, logPair("cacheTable", queryInput.getTableName()));
}
}
}
}
//////////////////////////////////////////////////////////////////////////
// for records that were found, if they're too old, maybe re-fetch them //
//////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(recordsFoundInCache))
{
refreshCacheIfExpired(recordsFoundInCache, queryInput, queryOutput);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void refreshCacheIfExpired(List<QRecord> recordsFoundInCache, QueryInput queryInput, QueryOutput queryOutput) throws QException
{
QTableMetaData table = queryInput.getTable();
Integer expirationSeconds = table.getCacheOf().getExpirationSeconds();
if(expirationSeconds != null)
{
List<QRecord> expiredRecords = new ArrayList<>();
for(QRecord cachedRecord : recordsFoundInCache)
{
Instant cachedDate = cachedRecord.getValueInstant(table.getCacheOf().getCachedDateFieldName());
if(cachedDate == null || cachedDate.isBefore(Instant.now().minus(expirationSeconds, ChronoUnit.SECONDS)))
{
expiredRecords.add(cachedRecord);
}
}
if(CollectionUtils.nullSafeHasContents(expiredRecords))
{
Map<List<Serializable>, Serializable> uniqueKeyToPrimaryKeyMap = getUniqueKeyToPrimaryKeyMap(table.getPrimaryKeyField(), expiredRecords);
Set<List<Serializable>> uniqueKeyValuesToRefresh = uniqueKeyToPrimaryKeyMap.keySet();
////////////////////////////////////////////
// fetch records from original source now //
////////////////////////////////////////////
List<QRecord> recordsFromSource = tryToGetFromCacheSource(queryInput, uniqueKeyValuesToRefresh);
Set<List<Serializable>> uniqueKeyValuesInFoundRecords = getUniqueKeyValuesFromFoundRecords(recordsFromSource);
Set<List<Serializable>> missedUniqueKeyValues = getUniqueKeyValuesFromQuery();
missedUniqueKeyValues.retainAll(getUniqueKeyValuesFromFoundRecords(expiredRecords));
missedUniqueKeyValues.removeAll(uniqueKeyValuesInFoundRecords);
//////////////////////////////////////////////////////////////////////////////////////////////////////
// build records to cache - setting their original (from cache) ids back in them, so they'll update //
//////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> refreshedRecordsToReturn = recordsFromSource.stream()
.map(r ->
{
QRecord recordToCache = CacheUtils.mapSourceRecordToCacheRecord(table, r, activeCacheUseCase);
recordToCache.setValue(table.getPrimaryKeyField(), uniqueKeyToPrimaryKeyMap.get(getUniqueKeyValues(recordToCache)));
return (recordToCache);
})
.toList();
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if the records were found in the source, put it into the output object so returned back to caller //
///////////////////////////////////////////////////////////////////////////////////////////////////////
Map<List<Serializable>, QRecord> refreshedRecordsByUniqueKeyValues = refreshedRecordsToReturn.stream().collect(Collectors.toMap(this::getUniqueKeyValues, r -> r, (a, b) -> a));
ListIterator<QRecord> queryOutputListIterator = queryOutput.getRecords().listIterator();
while(queryOutputListIterator.hasNext())
{
QRecord originalRecord = queryOutputListIterator.next();
List<Serializable> recordUniqueKeyValues = getUniqueKeyValues(originalRecord);
QRecord refreshedRecord = refreshedRecordsByUniqueKeyValues.get(recordUniqueKeyValues);
if(refreshedRecord != null)
{
queryOutputListIterator.set(refreshedRecord);
}
else if(missedUniqueKeyValues.contains(recordUniqueKeyValues))
{
queryOutputListIterator.remove();
}
}
////////////////////////////////////////////////////////////////////////////
// for refreshed records which should be cached, update them in the cache //
////////////////////////////////////////////////////////////////////////////
List<QRecord> recordsToUpdate = refreshedRecordsToReturn.stream().filter(r -> CacheUtils.shouldCacheRecord(table, r)).toList();
if(CollectionUtils.nullSafeHasContents(recordsToUpdate))
{
try
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(queryInput.getTableName());
updateInput.setRecords(recordsToUpdate);
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
}
catch(Exception e)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't let an exception break this query - it (probably) just indicates some data that didn't get cached - so - that's generally "ok" //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error updating cached records", e, logPair("cacheTable", queryInput.getTableName()));
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the records were missed in the source - OR if they shouldn't be cached now, then mark them for deleting //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Set<Serializable> cachedRecordIdsToDelete = missedUniqueKeyValues.stream()
.map(uniqueKeyToPrimaryKeyMap::get)
.collect(Collectors.toSet());
cachedRecordIdsToDelete.addAll(refreshedRecordsToReturn.stream()
.filter(r -> !CacheUtils.shouldCacheRecord(table, r))
.map(r -> r.getValue(table.getPrimaryKeyField()))
.collect(Collectors.toSet()));
if(CollectionUtils.nullSafeHasContents(cachedRecordIdsToDelete))
{
/////////////////////////////////////////////////////////////////////////////////
// if the records are no longer in the source, then remove them from the cache //
/////////////////////////////////////////////////////////////////////////////////
try
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(queryInput.getTableName());
deleteInput.setPrimaryKeys(new ArrayList<>(cachedRecordIdsToDelete));
new DeleteAction().execute(deleteInput);
}
catch(Exception e)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// don't let an exception break this query - it (probably) just indicates some data that didn't get uncached - so - that's generally "ok" //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.warn("Error deleting cached records", e, logPair("cacheTable", queryInput.getTableName()));
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Set<List<Serializable>> getUniqueKeyValuesFromQuery()
{
Set<List<Serializable>> rs = new HashSet<>();
int noOfUniqueKeys = uniqueKeyValues.get(cacheUniqueKey.getFieldNames().get(0)).size();
for(int i = 0; i < noOfUniqueKeys; i++)
{
List<Serializable> values = new ArrayList<>();
for(String fieldName : cacheUniqueKey.getFieldNames())
{
values.add(uniqueKeyValues.get(fieldName).get(i));
}
////////////////////////////////////////////////////////////////////////////////
// critical - leave this here so hashCode from the list is correctly computed //
////////////////////////////////////////////////////////////////////////////////
rs.add(values);
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private Set<List<Serializable>> getUniqueKeyValuesFromFoundRecords(List<QRecord> records)
{
return (getUniqueKeyToPrimaryKeyMap("ignore", records).keySet());
}
/*******************************************************************************
**
*******************************************************************************/
private Map<List<Serializable>, Serializable> getUniqueKeyToPrimaryKeyMap(String primaryKeyField, List<QRecord> records)
{
Map<List<Serializable>, Serializable> rs = new HashMap<>();
for(QRecord record : records)
{
List<Serializable> uniqueKeyValues = getUniqueKeyValues(record);
rs.put(uniqueKeyValues, record.getValue(primaryKeyField));
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private List<Serializable> getUniqueKeyValues(QRecord record)
{
List<Serializable> uniqueKeyValues = new ArrayList<>();
for(String fieldName : cacheUniqueKey.getFieldNames())
{
uniqueKeyValues.add(record.getValue(fieldName));
}
return uniqueKeyValues;
}
/*******************************************************************************
** figure out if this was a request that we can cache records for -
** e.g., if it's a request for unique-key EQUALS or IN
** build up fields for the unique keys, the values, etc.
*******************************************************************************/
private void analyzeInput(QueryInput queryInput)
{
QTableMetaData table = queryInput.getTable();
for(CacheUseCase cacheUseCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
cacheUseCaseMap.put(cacheUseCase.getType(), cacheUseCase);
}
if(cacheUseCaseMap.containsKey(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY))
{
if(queryInput.getFilter() == null)
{
LOG.trace("Unable to cache: there is no filter");
return;
}
QQueryFilter filter = queryInput.getFilter();
Set<String> queryFields = new HashSet<>();
if(CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
{
if(CollectionUtils.nullSafeHasContents(filter.getCriteria()))
{
LOG.trace("Unable to cache: we have sub-filters and criteria");
return;
}
if(!QQueryFilter.BooleanOperator.OR.equals(filter.getBooleanOperator()))
{
LOG.trace("Unable to cache: we have sub-filters but not an OR query");
return;
}
/////////////////////////
// look at sub-filters //
/////////////////////////
for(QQueryFilter subFilter : filter.getSubFilters())
{
Set<String> thisSubFilterFields = getQueryFieldsIfCacheableFilter(subFilter, false);
if(thisSubFilterFields == null)
{
return;
}
if(queryFields.isEmpty())
{
queryFields.addAll(thisSubFilterFields);
}
else
{
if(!queryFields.equals(thisSubFilterFields))
{
LOG.trace("Unable to cache: sub-filters have different sets of fields");
return;
}
}
}
if(doQueryFieldsMatchAUniqueKey(table, queryFields))
{
return;
}
LOG.trace("Unable to cache: we have sub-filters that do match a unique key");
return;
}
else
{
//////////////////////////////////////////
// look at the criteria in the query: //
// - build a set of field names //
// - fail upon unsupported operators //
// - collect the values in the criteria //
//////////////////////////////////////////
queryFields = getQueryFieldsIfCacheableFilter(filter, true);
if(queryFields == null)
{
return;
}
}
if(doQueryFieldsMatchAUniqueKey(table, queryFields))
{
return;
}
LOG.trace("Unable to cache: we have query fields that don't match a unique key: " + queryFields);
return;
}
LOG.trace("Unable to cache: No supported use case: " + cacheUseCaseMap.keySet());
}
/*******************************************************************************
**
*******************************************************************************/
private boolean doQueryFieldsMatchAUniqueKey(QTableMetaData table, Set<String> queryFields)
{
for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys()))
{
if(queryFields.equals(new HashSet<>(uniqueKey.getFieldNames())))
{
this.cacheUniqueKey = uniqueKey;
isQueryInputCacheable = true;
activeCacheUseCase = cacheUseCaseMap.get(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY);
return true;
}
}
return false;
}
/*******************************************************************************
**
*******************************************************************************/
private Set<String> getQueryFieldsIfCacheableFilter(QQueryFilter filter, boolean allowOperatorIn)
{
Set<String> rs = new HashSet<>();
for(QFilterCriteria criterion : filter.getCriteria())
{
boolean isEquals = criterion.getOperator().equals(QCriteriaOperator.EQUALS);
boolean isIn = criterion.getOperator().equals(QCriteriaOperator.IN);
if(isEquals || (isIn && allowOperatorIn))
{
rs.add(criterion.getFieldName());
this.uniqueKeyValues.addAll(criterion.getFieldName(), criterion.getValues());
}
else
{
LOG.trace("Unable to cache: we have an unsupported criteria operator: " + criterion.getOperator());
isQueryInputCacheable = false;
return (null);
}
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> tryToGetFromCacheSource(QueryInput queryInput, Set<List<Serializable>> uniqueKeyValues) throws QException
{
List<QRecord> recordsFromSource = null;
QTableMetaData table = queryInput.getTable();
if(CacheUseCase.Type.UNIQUE_KEY_TO_UNIQUE_KEY.equals(activeCacheUseCase.getType()))
{
recordsFromSource = getFromCachedSourceForUniqueKeyToUniqueKey(queryInput, uniqueKeyValues, table.getCacheOf().getSourceTable());
}
else
{
// todo!!
throw (new NotImplementedException("Not-yet-implemented cache use case type: " + activeCacheUseCase.getType()));
}
return (recordsFromSource);
}
/*******************************************************************************
**
*******************************************************************************/
private List<QRecord> getFromCachedSourceForUniqueKeyToUniqueKey(QueryInput cacheQueryInput, Set<List<Serializable>> uniqueKeyValues, String sourceTableName) throws QException
{
QTableMetaData sourceTable = QContext.getQInstance().getTable(sourceTableName);
QBackendMetaData sourceBackend = QContext.getQInstance().getBackendForTable(sourceTableName);
if(sourceTable.isCapabilityEnabled(sourceBackend, Capability.TABLE_QUERY))
{
///////////////////////////////////////////////////////
// do a Query on the source table, by the unique key //
///////////////////////////////////////////////////////
QueryInput sourceQueryInput = new QueryInput();
sourceQueryInput.setTableName(sourceTableName);
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
sourceQueryInput.setFilter(filter);
sourceQueryInput.setCommonParamsFrom(cacheQueryInput);
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
{
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
for(int i = 0; i < cacheUniqueKey.getFieldNames().size(); i++)
{
subFilter.addCriteria(new QFilterCriteria(cacheUniqueKey.getFieldNames().get(i), QCriteriaOperator.EQUALS, uniqueKeyValue.get(i)));
}
}
QueryOutput sourceQueryOutput = new QueryAction().execute(sourceQueryInput);
return (sourceQueryOutput.getRecords());
}
else if(sourceTable.isCapabilityEnabled(sourceBackend, Capability.TABLE_GET))
{
///////////////////////////////////////////////////////////////////////
// if the table only supports GET, then do a GET for each unique key //
///////////////////////////////////////////////////////////////////////
List<QRecord> outputRecords = new ArrayList<>();
for(List<Serializable> uniqueKeyValue : uniqueKeyValues)
{
Map<String, Serializable> uniqueKey = new HashMap<>();
for(int i = 0; i < cacheUniqueKey.getFieldNames().size(); i++)
{
uniqueKey.put(cacheUniqueKey.getFieldNames().get(i), uniqueKeyValue.get(i));
}
GetInput getInput = new GetInput();
getInput.setTableName(sourceTableName);
getInput.setUniqueKey(uniqueKey);
getInput.setCommonParamsFrom(cacheQueryInput);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
outputRecords.add(getOutput.getRecord());
}
}
return (outputRecords);
}
else
{
throw (new QException("Cache source table " + sourceTableName + " does not support Query or Get capability."));
}
}
}

View File

@ -31,16 +31,12 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -54,11 +50,9 @@ import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -363,8 +357,8 @@ public class QueryStatManager
//////////////////////
// set the table id //
//////////////////////
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
queryStat.setQqqTableId(qqqTableId);
Integer tableId = QQQTableAccessor.getTableId(queryStat.getTableName());
queryStat.setTableId(tableId);
//////////////////////////////
// build join-table records //
@ -374,7 +368,7 @@ public class QueryStatManager
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
for(String joinTableName : queryStat.getJoinTableNames())
{
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
queryStatJoinTableList.add(new QueryStatJoinTable().withTableId(QQQTableAccessor.getTableId(joinTableName)));
}
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
}
@ -385,14 +379,14 @@ public class QueryStatManager
if(queryStat.getQueryFilter() != null && queryStat.getQueryFilter().hasAnyCriteria())
{
List<QueryStatCriteriaField> queryStatCriteriaFieldList = new ArrayList<>();
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, queryStat.getQueryFilter());
processCriteriaFromFilter(tableId, queryStatCriteriaFieldList, queryStat.getQueryFilter());
queryStat.setQueryStatCriteriaFieldList(queryStatCriteriaFieldList);
}
if(CollectionUtils.nullSafeHasContents(queryStat.getQueryFilter().getOrderBys()))
{
List<QueryStatOrderByField> queryStatOrderByFieldList = new ArrayList<>();
processOrderByFromFilter(qqqTableId, queryStatOrderByFieldList, queryStat.getQueryFilter());
processOrderByFromFilter(tableId, queryStatOrderByFieldList, queryStat.getQueryFilter());
queryStat.setQueryStatOrderByFieldList(queryStatOrderByFieldList);
}
@ -434,7 +428,7 @@ public class QueryStatManager
/*******************************************************************************
**
*******************************************************************************/
private static void processCriteriaFromFilter(Integer qqqTableId, List<QueryStatCriteriaField> queryStatCriteriaFieldList, QQueryFilter queryFilter) throws QException
private static void processCriteriaFromFilter(Integer tableId, List<QueryStatCriteriaField> queryStatCriteriaFieldList, QQueryFilter queryFilter) throws QException
{
for(QFilterCriteria criteria : CollectionUtils.nonNullList(queryFilter.getCriteria()))
{
@ -452,13 +446,13 @@ public class QueryStatManager
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
queryStatCriteriaField.setTableId(QQQTableAccessor.getTableId(parts[0]));
queryStatCriteriaField.setName(parts[1]);
}
}
else
{
queryStatCriteriaField.setQqqTableId(qqqTableId);
queryStatCriteriaField.setTableId(tableId);
queryStatCriteriaField.setName(fieldName);
}
@ -467,7 +461,7 @@ public class QueryStatManager
for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
{
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, subFilter);
processCriteriaFromFilter(tableId, queryStatCriteriaFieldList, subFilter);
}
}
@ -476,7 +470,7 @@ public class QueryStatManager
/*******************************************************************************
**
*******************************************************************************/
private static void processOrderByFromFilter(Integer qqqTableId, List<QueryStatOrderByField> queryStatOrderByFieldList, QQueryFilter queryFilter) throws QException
private static void processOrderByFromFilter(Integer tableId, List<QueryStatOrderByField> queryStatOrderByFieldList, QQueryFilter queryFilter) throws QException
{
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
{
@ -490,13 +484,13 @@ public class QueryStatManager
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
queryStatOrderByField.setTableId(QQQTableAccessor.getTableId(parts[0]));
queryStatOrderByField.setName(parts[1]);
}
}
else
{
queryStatOrderByField.setQqqTableId(qqqTableId);
queryStatOrderByField.setTableId(tableId);
queryStatOrderByField.setName(fieldName);
}
@ -505,43 +499,6 @@ public class QueryStatManager
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer getQQQTableId(String tableName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", tableName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord().getValueInteger("id");
}
}

View File

@ -26,8 +26,8 @@ import java.nio.file.Path;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfInput;
import com.kingsrook.qqq.backend.core.model.templates.ConvertHtmlToPdfOutput;
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.ConvertHtmlToPdfOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

View File

@ -28,8 +28,8 @@ import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.velocity.VelocityContext;
@ -62,7 +62,15 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
if(TemplateType.VELOCITY.equals(input.getTemplateType()))
{
Velocity.init();
Context context = new VelocityContext(input.getContext());
Context context = new VelocityContext();
if(input.getContext() != null)
{
for(Map.Entry<String, ?> entry : input.getContext().entrySet())
{
context.put(entry.getKey(), entry.getValue());
}
}
setupEventHandlers(context);

View File

@ -100,7 +100,7 @@ public class QInstanceEnricher
//////////////////////////////////////////////////////////////////////////////////////////////////
// let an instance define mappings to be applied during name-to-label enrichments, //
// e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" shoudl always be "TLA") //
// e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" should always be "TLA") //
// or to expand abbreviations in code (e.g., "Addr" should always be "Address" //
//////////////////////////////////////////////////////////////////////////////////////////////////
private static final Map<String, String> labelMappings = new LinkedHashMap<>();

View File

@ -1224,11 +1224,10 @@ public class QInstanceValidator
QScheduleMetaData schedule = process.getSchedule();
assertCondition(schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null, "Either repeat millis or repeat seconds must be set on schedule in process " + processName);
if(schedule.getBackendVariant() != null)
if(schedule.getVariantBackend() != null)
{
assertCondition(schedule.getVariantRunStrategy() != null, "A variant strategy was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(schedule.getVariantTableName() != null, "A variant table name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(schedule.getVariantFieldName() != null, "A variant field name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(qInstance.getBackend(schedule.getVariantBackend()) != null, "A variant backend was not found for " + schedule.getVariantBackend());
assertCondition(schedule.getVariantRunStrategy() != null, "A variant run strategy was not set for " + schedule.getVariantBackend() + " on schedule in process " + processName);
}
}

View File

@ -155,14 +155,4 @@ public class AbstractActionInput
this.asyncJobCallback = asyncJobCallback;
}
/*******************************************************************************
** Fluent setter for instance
*******************************************************************************/
public AbstractActionInput withInstance(QInstance instance)
{
return (this);
}
}

View File

@ -0,0 +1,149 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables;
import java.util.Collection;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
/*******************************************************************************
** Common getters & setters, shared by both QueryInput and GetInput.
**
** Original impetus for this class is the setCommonParamsFrom() method - for cases
** where we need to change a Query to a Get, or vice-versa, and we want to copy over
** all of those input params.
*******************************************************************************/
public interface QueryOrGetInputInterface
{
/*******************************************************************************
** Set in THIS, the "common params" (e.g., common to both Query & Get inputs)
** from the parameter SOURCE object.
*******************************************************************************/
default void setCommonParamsFrom(QueryOrGetInputInterface source)
{
this.setTransaction(source.getTransaction());
this.setShouldTranslatePossibleValues(source.getShouldTranslatePossibleValues());
this.setShouldGenerateDisplayValues(source.getShouldGenerateDisplayValues());
this.setShouldFetchHeavyFields(source.getShouldFetchHeavyFields());
this.setShouldOmitHiddenFields(source.getShouldOmitHiddenFields());
this.setShouldMaskPasswords(source.getShouldMaskPasswords());
this.setIncludeAssociations(source.getIncludeAssociations());
this.setAssociationNamesToInclude(source.getAssociationNamesToInclude());
}
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
QBackendTransaction getTransaction();
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
void setTransaction(QBackendTransaction transaction);
/*******************************************************************************
** Getter for shouldTranslatePossibleValues
*******************************************************************************/
boolean getShouldTranslatePossibleValues();
/*******************************************************************************
** Setter for shouldTranslatePossibleValues
*******************************************************************************/
void setShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues);
/*******************************************************************************
** Getter for shouldGenerateDisplayValues
*******************************************************************************/
boolean getShouldGenerateDisplayValues();
/*******************************************************************************
** Setter for shouldGenerateDisplayValues
*******************************************************************************/
void setShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues);
/*******************************************************************************
** Getter for shouldFetchHeavyFields
*******************************************************************************/
boolean getShouldFetchHeavyFields();
/*******************************************************************************
** Setter for shouldFetchHeavyFields
*******************************************************************************/
void setShouldFetchHeavyFields(boolean shouldFetchHeavyFields);
/*******************************************************************************
** Getter for shouldOmitHiddenFields
*******************************************************************************/
boolean getShouldOmitHiddenFields();
/*******************************************************************************
** Setter for shouldOmitHiddenFields
*******************************************************************************/
void setShouldOmitHiddenFields(boolean shouldOmitHiddenFields);
/*******************************************************************************
** Getter for shouldMaskPasswords
*******************************************************************************/
boolean getShouldMaskPasswords();
/*******************************************************************************
** Setter for shouldMaskPasswords
*******************************************************************************/
void setShouldMaskPasswords(boolean shouldMaskPasswords);
/*******************************************************************************
** Getter for includeAssociations
*******************************************************************************/
boolean getIncludeAssociations();
/*******************************************************************************
** Setter for includeAssociations
*******************************************************************************/
void setIncludeAssociations(boolean includeAssociations);
/*******************************************************************************
** Getter for associationNamesToInclude
*******************************************************************************/
Collection<String> getAssociationNamesToInclude();
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
void setAssociationNamesToInclude(Collection<String> associationNamesToInclude);
}

View File

@ -40,6 +40,8 @@ public class AggregateInput extends AbstractTableActionInput
private List<GroupBy> groupBys = new ArrayList<>();
private Integer limit;
private Integer timeoutSeconds;
private List<QueryJoin> queryJoins = null;
@ -269,4 +271,35 @@ public class AggregateInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for timeoutSeconds
*******************************************************************************/
public Integer getTimeoutSeconds()
{
return (this.timeoutSeconds);
}
/*******************************************************************************
** Setter for timeoutSeconds
*******************************************************************************/
public void setTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
}
/*******************************************************************************
** Fluent setter for timeoutSeconds
*******************************************************************************/
public AggregateInput withTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
return (this);
}
}

View File

@ -37,6 +37,8 @@ public class CountInput extends AbstractTableActionInput
{
private QQueryFilter filter;
private Integer timeoutSeconds;
private List<QueryJoin> queryJoins = null;
private Boolean includeDistinctCount = false;
@ -51,6 +53,17 @@ public class CountInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public CountInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
** Getter for filter
**
@ -152,4 +165,46 @@ public class CountInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public CountInput withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for timeoutSeconds
*******************************************************************************/
public Integer getTimeoutSeconds()
{
return (this.timeoutSeconds);
}
/*******************************************************************************
** Setter for timeoutSeconds
*******************************************************************************/
public void setTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
}
/*******************************************************************************
** Fluent setter for timeoutSeconds
*******************************************************************************/
public CountInput withTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
return (this);
}
}

View File

@ -43,6 +43,9 @@ public class DeleteInput extends AbstractTableActionInput
private QQueryFilter queryFilter;
private InputSource inputSource = QInputSource.SYSTEM;
private boolean omitDmlAudit = false;
private String auditContext = null;
/*******************************************************************************
@ -54,6 +57,29 @@ public class DeleteInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DeleteInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public DeleteInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
** Getter for transaction
**
@ -188,4 +214,66 @@ public class DeleteInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for omitDmlAudit
*******************************************************************************/
public boolean getOmitDmlAudit()
{
return (this.omitDmlAudit);
}
/*******************************************************************************
** Setter for omitDmlAudit
*******************************************************************************/
public void setOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
}
/*******************************************************************************
** Fluent setter for omitDmlAudit
*******************************************************************************/
public DeleteInput withOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
return (this);
}
/*******************************************************************************
** Getter for auditContext
*******************************************************************************/
public String getAuditContext()
{
return (this.auditContext);
}
/*******************************************************************************
** Setter for auditContext
*******************************************************************************/
public void setAuditContext(String auditContext)
{
this.auditContext = auditContext;
}
/*******************************************************************************
** Fluent setter for auditContext
*******************************************************************************/
public DeleteInput withAuditContext(String auditContext)
{
this.auditContext = auditContext;
return (this);
}
}

View File

@ -27,13 +27,14 @@ import java.util.Collection;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
/*******************************************************************************
** Input data for the Get action
**
*******************************************************************************/
public class GetInput extends AbstractTableActionInput
public class GetInput extends AbstractTableActionInput implements QueryOrGetInputInterface
{
private QBackendTransaction transaction;
@ -66,6 +67,29 @@ public class GetInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public GetInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AbstractTableActionInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
** Getter for primaryKey
**

View File

@ -22,12 +22,15 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.insert;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -43,6 +46,7 @@ public class InsertInput extends AbstractTableActionInput
private boolean skipUniqueKeyCheck = false;
private boolean omitDmlAudit = false;
private String auditContext = null;
@ -55,6 +59,71 @@ public class InsertInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public InsertInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecord(QRecord record)
{
if(records == null)
{
records = new ArrayList<>();
}
records.add(record);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecordEntity(QRecordEntity recordEntity)
{
return (withRecord(recordEntity.toQRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
public InsertInput withRecordEntities(List<QRecordEntity> recordEntityList)
{
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
{
withRecordEntity(recordEntity);
}
return (this);
}
/*******************************************************************************
** Getter for transaction
**
@ -216,4 +285,35 @@ public class InsertInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for auditContext
*******************************************************************************/
public String getAuditContext()
{
return (this.auditContext);
}
/*******************************************************************************
** Setter for auditContext
*******************************************************************************/
public void setAuditContext(String auditContext)
{
this.auditContext = auditContext;
}
/*******************************************************************************
** Fluent setter for auditContext
*******************************************************************************/
public InsertInput withAuditContext(String auditContext)
{
this.auditContext = auditContext;
return (this);
}
}

View File

@ -26,8 +26,9 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization.QFilterCriteriaDeserializer;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -36,6 +37,7 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils;
* A single criteria Component of a Query
*
*******************************************************************************/
@JsonDeserialize(using = QFilterCriteriaDeserializer.class)
public class QFilterCriteria implements Serializable, Cloneable
{
private static final QLogger LOG = QLogger.getLogger(QFilterCriteria.class);
@ -44,8 +46,10 @@ public class QFilterCriteria implements Serializable, Cloneable
private QCriteriaOperator operator;
private List<Serializable> values;
private String otherFieldName;
private AbstractFilterExpression<?> expression;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - probably implement this as a type of expression - though would require a little special handling i think when evaluating... //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private String otherFieldName;
@ -98,23 +102,6 @@ public class QFilterCriteria implements Serializable, Cloneable
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
{
this.fieldName = fieldName;
this.operator = operator;
this.expression = expression;
///////////////////////////////////////
// this guy doesn't like to be null? //
///////////////////////////////////////
this.values = new ArrayList<>();
}
/*******************************************************************************
**
*******************************************************************************/
@ -332,35 +319,4 @@ public class QFilterCriteria implements Serializable, Cloneable
return (rs.toString());
}
/*******************************************************************************
** Getter for expression
*******************************************************************************/
public AbstractFilterExpression<?> getExpression()
{
return (this.expression);
}
/*******************************************************************************
** Setter for expression
*******************************************************************************/
public void setExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
}
/*******************************************************************************
** Fluent setter for expression
*******************************************************************************/
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
return (this);
}
}

View File

@ -29,18 +29,20 @@ import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
/*******************************************************************************
** Input data for the Query action
**
*******************************************************************************/
public class QueryInput extends AbstractTableActionInput
public class QueryInput extends AbstractTableActionInput implements QueryOrGetInputInterface
{
private QBackendTransaction transaction;
private QQueryFilter filter;
private RecordPipe recordPipe;
private Integer timeoutSeconds;
private boolean shouldTranslatePossibleValues = false;
private boolean shouldGenerateDisplayValues = false;
@ -77,6 +79,17 @@ public class QueryInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QueryInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
** Getter for filter
**
@ -525,4 +538,35 @@ public class QueryInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for timeoutSeconds
*******************************************************************************/
public Integer getTimeoutSeconds()
{
return (this.timeoutSeconds);
}
/*******************************************************************************
** Setter for timeoutSeconds
*******************************************************************************/
public void setTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
}
/*******************************************************************************
** Fluent setter for timeoutSeconds
*******************************************************************************/
public QueryInput withTimeoutSeconds(Integer timeoutSeconds)
{
this.timeoutSeconds = timeoutSeconds;
return (this);
}
}

View File

@ -28,10 +28,31 @@ import java.io.Serializable;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractFilterExpression<T extends Serializable>
public abstract class AbstractFilterExpression<T extends Serializable> implements Serializable
{
/*******************************************************************************
**
*******************************************************************************/
public abstract T evaluate();
/*******************************************************************************
** To help with serialization, define a "type" in all subclasses
*******************************************************************************/
public String getType()
{
return (getClass().getSimpleName());
}
/*******************************************************************************
** noop - but here so serialization won't be upset about there being a type
** in a json object.
*******************************************************************************/
public void setType(String type)
{
}
}

View File

@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
@ -31,9 +35,9 @@ import java.util.concurrent.TimeUnit;
*******************************************************************************/
public class NowWithOffset extends AbstractFilterExpression<Instant>
{
private final Operator operator;
private final int amount;
private final TimeUnit timeUnit;
private Operator operator;
private int amount;
private ChronoUnit timeUnit;
@ -46,7 +50,17 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Constructor
**
*******************************************************************************/
private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit)
public NowWithOffset()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private NowWithOffset(Operator operator, int amount, ChronoUnit timeUnit)
{
this.operator = operator;
this.amount = amount;
@ -59,7 +73,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Factory
**
*******************************************************************************/
@Deprecated
public static NowWithOffset minus(int amount, TimeUnit timeUnit)
{
return (minus(amount, timeUnit.toChronoUnit()));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset minus(int amount, ChronoUnit timeUnit)
{
return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
}
@ -70,7 +96,19 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Factory
**
*******************************************************************************/
@Deprecated
public static NowWithOffset plus(int amount, TimeUnit timeUnit)
{
return (plus(amount, timeUnit.toChronoUnit()));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset plus(int amount, ChronoUnit timeUnit)
{
return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
}
@ -83,14 +121,24 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
@Override
public Instant evaluate()
{
/////////////////////////////////////////////////////////////////////////////
// Instant doesn't let us plus/minus WEEK, MONTH, or YEAR... //
// but LocalDateTime does. So, make a LDT in UTC, do the plus/minus, then //
// convert back to Instant @ UTC //
/////////////////////////////////////////////////////////////////////////////
LocalDateTime now = LocalDateTime.now(ZoneId.of("UTC"));
LocalDateTime then;
if(operator.equals(Operator.PLUS))
{
return (Instant.now().plus(amount, timeUnit.toChronoUnit()));
then = now.plus(amount, timeUnit);
}
else
{
return (Instant.now().minus(amount, timeUnit.toChronoUnit()));
then = now.minus(amount, timeUnit);
}
return (then.toInstant(ZoneOffset.UTC));
}
@ -121,7 +169,7 @@ public class NowWithOffset extends AbstractFilterExpression<Instant>
** Getter for timeUnit
**
*******************************************************************************/
public TimeUnit getTimeUnit()
public ChronoUnit getTimeUnit()
{
return timeUnit;
}

View File

@ -0,0 +1,178 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ThisOrLastPeriod extends AbstractFilterExpression<Instant>
{
private Operator operator;
private ChronoUnit timeUnit;
public enum Operator
{THIS, LAST}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ThisOrLastPeriod()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private ThisOrLastPeriod(Operator operator, ChronoUnit timeUnit)
{
this.operator = operator;
this.timeUnit = timeUnit;
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod this_(ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.THIS, timeUnit));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static ThisOrLastPeriod last(int amount, ChronoUnit timeUnit)
{
return (new ThisOrLastPeriod(Operator.LAST, timeUnit));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate()
{
ZoneId zoneId = ValueUtils.getSessionOrInstanceZoneId();
switch(timeUnit)
{
case HOURS ->
{
if(operator.equals(Operator.THIS))
{
return Instant.now().truncatedTo(ChronoUnit.HOURS);
}
else
{
return Instant.now().minus(1, ChronoUnit.HOURS).truncatedTo(ChronoUnit.HOURS);
}
}
case DAYS ->
{
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
return operator.equals(Operator.THIS) ? startOfToday : startOfToday.minus(1, ChronoUnit.DAYS);
}
case WEEKS ->
{
Instant startOfToday = ValueUtils.getStartOfTodayInZoneId(zoneId.getId());
LocalDateTime startOfThisWeekLDT = LocalDateTime.ofInstant(startOfToday, zoneId);
while(startOfThisWeekLDT.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
////////////////////////////////////////
// go backwards until sunday is found //
////////////////////////////////////////
startOfThisWeekLDT = startOfThisWeekLDT.minus(1, ChronoUnit.DAYS);
}
Instant startOfThisWeek = startOfThisWeekLDT.toInstant(zoneId.getRules().getOffset(startOfThisWeekLDT));
return operator.equals(Operator.THIS) ? startOfThisWeek : startOfThisWeek.minus(7, ChronoUnit.DAYS);
}
case MONTHS ->
{
Instant startOfThisMonth = ValueUtils.getStartOfMonthInZoneId(zoneId.getId());
LocalDateTime startOfThisMonthLDT = LocalDateTime.ofInstant(startOfThisMonth, ZoneId.of(zoneId.getId()));
LocalDateTime startOfLastMonthLDT = startOfThisMonthLDT.minus(1, ChronoUnit.MONTHS);
Instant startOfLastMonth = startOfLastMonthLDT.toInstant(ZoneId.of(zoneId.getId()).getRules().getOffset(Instant.now()));
return operator.equals(Operator.THIS) ? startOfThisMonth : startOfLastMonth;
}
case YEARS ->
{
Instant startOfThisYear = ValueUtils.getStartOfYearInZoneId(zoneId.getId());
LocalDateTime startOfThisYearLDT = LocalDateTime.ofInstant(startOfThisYear, zoneId);
LocalDateTime startOfLastYearLDT = startOfThisYearLDT.minus(1, ChronoUnit.YEARS);
Instant startOfLastYear = startOfLastYearLDT.toInstant(zoneId.getRules().getOffset(Instant.now()));
return operator.equals(Operator.THIS) ? startOfThisYear : startOfLastYear;
}
default -> throw (new QRuntimeException("Unsupported timeUnit: " + timeUnit));
}
}
/*******************************************************************************
** Getter for operator
**
*******************************************************************************/
public Operator getOperator()
{
return operator;
}
/*******************************************************************************
** Getter for timeUnit
**
*******************************************************************************/
public ChronoUnit getTimeUnit()
{
return timeUnit;
}
}

View File

@ -0,0 +1,129 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.serialization;
import java.io.IOException;
import java.io.Serializable;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Custom jackson deserializer, to deal w/ abstract expression field
*******************************************************************************/
public class QFilterCriteriaDeserializer extends StdDeserializer<QFilterCriteria>
{
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteriaDeserializer()
{
this(null);
}
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteriaDeserializer(Class<?> vc)
{
super(vc);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QFilterCriteria deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException
{
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
ObjectMapper objectMapper = new ObjectMapper();
/////////////////////////////////
// get values out of json node //
/////////////////////////////////
List<Serializable> values = objectMapper.treeToValue(node.get("values"), List.class);
String fieldName = objectMapper.treeToValue(node.get("fieldName"), String.class);
QCriteriaOperator operator = objectMapper.treeToValue(node.get("operator"), QCriteriaOperator.class);
String otherFieldName = objectMapper.treeToValue(node.get("otherFieldName"), String.class);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// look at all the values - if any of them are actually meant to be an Expression (instance of subclass of AbstractFilterExpression) //
// they'll have deserialized as a Map, with a "type" key. If that's the case, then re/de serialize them into the proper expression type //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ListIterator<Serializable> valuesIterator = CollectionUtils.nonNullList(values).listIterator();
while(valuesIterator.hasNext())
{
Object value = valuesIterator.next();
if(value instanceof Map<?, ?> map && map.containsKey("type"))
{
String expressionType = ValueUtils.getValueAsString(map.get("type"));
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// right now, we'll assume that all expression subclasses are in the same package as AbstractFilterExpression //
// so, we can just do a Class.forName on that name, and use JsonUtils.toObject requesting that class. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
try
{
String assumedExpressionJSON = JsonUtils.toJson(map);
String className = AbstractFilterExpression.class.getName().replace(AbstractFilterExpression.class.getSimpleName(), expressionType);
Serializable replacementValue = (Serializable) JsonUtils.toObject(assumedExpressionJSON, Class.forName(className));
valuesIterator.set(replacementValue);
}
catch(Exception e)
{
throw (new IOException("Error deserializing criteria value which appeared to be an expression of type [" + expressionType + "] inside QFilterCriteria", e));
}
}
}
///////////////////////////////////
// put fields into return object //
///////////////////////////////////
QFilterCriteria criteria = new QFilterCriteria();
criteria.setFieldName(fieldName);
criteria.setOperator(operator);
criteria.setValues(values);
criteria.setOtherFieldName(otherFieldName);
return (criteria);
}
}

View File

@ -0,0 +1,210 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.replace;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
**
*******************************************************************************/
public class ReplaceInput extends AbstractTableActionInput
{
private QBackendTransaction transaction;
private UniqueKey key;
private List<QRecord> records;
private QQueryFilter filter;
private boolean omitDmlAudit = false;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ReplaceInput()
{
}
/*******************************************************************************
** Getter for transaction
*******************************************************************************/
public QBackendTransaction getTransaction()
{
return (this.transaction);
}
/*******************************************************************************
** Setter for transaction
*******************************************************************************/
public void setTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
}
/*******************************************************************************
** Fluent setter for transaction
*******************************************************************************/
public ReplaceInput withTransaction(QBackendTransaction transaction)
{
this.transaction = transaction;
return (this);
}
/*******************************************************************************
** Getter for records
*******************************************************************************/
public List<QRecord> getRecords()
{
return (this.records);
}
/*******************************************************************************
** Setter for records
*******************************************************************************/
public void setRecords(List<QRecord> records)
{
this.records = records;
}
/*******************************************************************************
** Fluent setter for records
*******************************************************************************/
public ReplaceInput withRecords(List<QRecord> records)
{
this.records = records;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public ReplaceInput withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for key
*******************************************************************************/
public UniqueKey getKey()
{
return (this.key);
}
/*******************************************************************************
** Setter for key
*******************************************************************************/
public void setKey(UniqueKey key)
{
this.key = key;
}
/*******************************************************************************
** Fluent setter for key
*******************************************************************************/
public ReplaceInput withKey(UniqueKey key)
{
this.key = key;
return (this);
}
/*******************************************************************************
** Getter for omitDmlAudit
*******************************************************************************/
public boolean getOmitDmlAudit()
{
return (this.omitDmlAudit);
}
/*******************************************************************************
** Setter for omitDmlAudit
*******************************************************************************/
public void setOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
}
/*******************************************************************************
** Fluent setter for omitDmlAudit
*******************************************************************************/
public ReplaceInput withOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
return (this);
}
}

View File

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

View File

@ -22,12 +22,15 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.update;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.InputSource;
import com.kingsrook.qqq.backend.core.model.actions.tables.QInputSource;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
@ -62,6 +65,71 @@ public class UpdateInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public UpdateInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public UpdateInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecord(QRecord record)
{
if(records == null)
{
records = new ArrayList<>();
}
records.add(record);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecordEntity(QRecordEntity recordEntity)
{
return (withRecord(recordEntity.toQRecord()));
}
/*******************************************************************************
**
*******************************************************************************/
public UpdateInput withRecordEntities(List<QRecordEntity> recordEntityList)
{
for(QRecordEntity recordEntity : CollectionUtils.nonNullList(recordEntityList))
{
withRecordEntity(recordEntity);
}
return (this);
}
/*******************************************************************************
** Getter for transaction
**

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.templates;
package com.kingsrook.qqq.backend.core.model.actions.templates;
import java.io.OutputStream;

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.templates;
package com.kingsrook.qqq.backend.core.model.actions.templates;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,11 +19,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.templates;
package com.kingsrook.qqq.backend.core.model.actions.templates;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;
/*******************************************************************************
@ -35,7 +36,7 @@ public class RenderTemplateInput extends AbstractActionInput
private String code; // todo - TemplateReference, like CodeReference??
private TemplateType templateType;
private Map<String, Object> context;
private Map<String, ? extends Object> context;
@ -120,7 +121,7 @@ public class RenderTemplateInput extends AbstractActionInput
** Getter for context
**
*******************************************************************************/
public Map<String, Object> getContext()
public Map<String, ? extends Object> getContext()
{
return context;
}
@ -131,7 +132,7 @@ public class RenderTemplateInput extends AbstractActionInput
** Setter for context
**
*******************************************************************************/
public void setContext(Map<String, Object> context)
public void setContext(Map<String, ? extends Object> context)
{
this.context = context;
}

View File

@ -1,6 +1,6 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.templates;
package com.kingsrook.qqq.backend.core.model.actions.templates;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;

View File

@ -29,7 +29,6 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
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.fields.ValueTooLongBehavior;
@ -37,13 +36,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.processes.implementations.audits.GetRecordAuditsStep;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
@ -51,14 +47,9 @@ import com.kingsrook.qqq.backend.core.processes.implementations.audits.GetRecord
*******************************************************************************/
public class AuditsMetaDataProvider
{
public static final String TABLE_NAME_AUDIT_TABLE = "auditTable";
public static final String TABLE_NAME_AUDIT_USER = "auditUser";
public static final String TABLE_NAME_AUDIT = "audit";
public static final String TABLE_NAME_AUDIT_DETAIL = "auditDetail";
public static final String TABLE_NAME_AUDIT_TREE = "auditTree";
public static final String AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT = "auditTreeJoinAuditTableForRoot";
public static final String AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_NODE = "auditTreeJoinAuditTableForNode";
@ -70,21 +61,6 @@ public class AuditsMetaDataProvider
defineStandardAuditTables(instance, backendName, backendDetailEnricher);
defineStandardAuditPossibleValueSources(instance);
defineStandardAuditJoins(instance);
defineProcessGetRecordAudits(instance);
}
/*******************************************************************************
**
*******************************************************************************/
private void defineProcessGetRecordAudits(QInstance instance)
{
instance.addProcess(new QProcessMetaData()
.withName("getRecordAudits")
.withStepList(List.of(new QBackendStepMetaData()
.withName("step")
.withCode(new QCodeReference(GetRecordAuditsStep.class)))));
}
@ -96,10 +72,10 @@ public class AuditsMetaDataProvider
{
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT)
.withRightTable(TABLE_NAME_AUDIT_TABLE)
.withRightTable(QQQTable.TABLE_NAME)
.withInferredName()
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("auditTableId", "id")));
.withJoinOn(new JoinOn("tableId", "id")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT)
@ -115,20 +91,6 @@ public class AuditsMetaDataProvider
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "auditId")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT_TREE)
.withRightTable(TABLE_NAME_AUDIT_TABLE)
.withName(AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("rootAuditTableId", "id")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT_TREE)
.withRightTable(TABLE_NAME_AUDIT_TABLE)
.withName(AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_NODE)
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("nodeAuditTableId", "id")));
}
@ -151,11 +113,6 @@ public class AuditsMetaDataProvider
*******************************************************************************/
public void defineStandardAuditPossibleValueSources(QInstance instance)
{
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT_TABLE)
.withTableName(TABLE_NAME_AUDIT_TABLE)
);
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT_USER)
.withTableName(TABLE_NAME_AUDIT_USER)
@ -176,10 +133,8 @@ public class AuditsMetaDataProvider
{
List<QTableMetaData> rs = new ArrayList<>();
rs.add(enrich(backendDetailEnricher, defineAuditUserTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditDetailTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTreeTable(backendName)));
return (rs);
}
@ -199,29 +154,6 @@ public class AuditsMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineAuditTableTable(String backendName)
{
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT_TABLE)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withPrimaryKeyField("id")
.withUniqueKey(new UniqueKey("name"))
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("label", QFieldType.STRING))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
/*******************************************************************************
**
*******************************************************************************/
@ -254,14 +186,10 @@ public class AuditsMetaDataProvider
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("auditTableId", "recordId")
.withRecordLabelFields("tableId", "recordId")
.withPrimaryKeyField("id")
.withAssociation(new Association()
.withName("auditDetails")
.withAssociatedTableName(TABLE_NAME_AUDIT_DETAIL)
.withJoinName(QJoinMetaData.makeInferredJoinName(TABLE_NAME_AUDIT, TABLE_NAME_AUDIT_DETAIL)))
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("auditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_TABLE))
.withField(new QFieldMetaData("tableId", QFieldType.INTEGER).withPossibleValueSourceName(QQQTable.TABLE_NAME))
.withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER))
.withField(new QFieldMetaData("recordId", QFieldType.INTEGER))
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
@ -292,26 +220,4 @@ public class AuditsMetaDataProvider
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineAuditTreeTable(String backendName)
{
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT_TREE)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("rootAuditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("rootRecordId", QFieldType.INTEGER))
.withField(new QFieldMetaData("nodeAuditTableId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("nodeRecordId", QFieldType.INTEGER))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
}

View File

@ -27,9 +27,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
@ -48,8 +48,8 @@ public class TableTrigger extends QRecordEntity
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField(possibleValueSourceName = SavedFilter.TABLE_NAME)
private Integer filterId;
@ -191,40 +191,6 @@ public class TableTrigger extends QRecordEntity
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Setter for tableName
**
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
**
*******************************************************************************/
public TableTrigger withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for filterId
**
@ -390,4 +356,35 @@ public class TableTrigger extends QRecordEntity
return (this);
}
/*******************************************************************************
** Getter for tableId
*******************************************************************************/
public Integer getTableId()
{
return (this.tableId);
}
/*******************************************************************************
** Setter for tableId
*******************************************************************************/
public void setTableId(Integer tableId)
{
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for tableId
*******************************************************************************/
public TableTrigger withTableId(Integer tableId)
{
this.tableId = tableId;
return (this);
}
}

View File

@ -68,11 +68,23 @@ public abstract class QRecordEntity
**
*******************************************************************************/
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord) throws QException
{
return (QRecordEntity.fromQRecord(c, qRecord, ""));
}
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord - where the fields for
** this entity have the given prefix - e.g., if they were selected as part of a join.
**
*******************************************************************************/
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord, String fieldNamePrefix) throws QException
{
try
{
T entity = c.getConstructor().newInstance();
entity.populateFromQRecord(qRecord);
entity.populateFromQRecord(qRecord, fieldNamePrefix);
return (entity);
}
catch(Exception e)
@ -88,14 +100,32 @@ public abstract class QRecordEntity
**
*******************************************************************************/
protected void populateFromQRecord(QRecord qRecord) throws QRuntimeException
{
populateFromQRecord(qRecord, "");
}
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord - where the fields for
** this entity have the given prefix - e.g., if they were selected as part of a join.
**
*******************************************************************************/
protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord, String fieldNamePrefix) throws QRuntimeException
{
try
{
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
originalRecordValues = new HashMap<>();
if(fieldNamePrefix == null)
{
fieldNamePrefix = "";
}
for(QRecordEntityField qRecordEntityField : fieldList)
{
Serializable value = qRecord.getValue(qRecordEntityField.getFieldName());
Serializable value = qRecord.getValue(fieldNamePrefix + qRecordEntityField.getFieldName());
Object typedValue = qRecordEntityField.convertValueType(value);
qRecordEntityField.getSetter().invoke(this, typedValue);
originalRecordValues.put(qRecordEntityField.getFieldName(), value);

View File

@ -32,6 +32,9 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
*******************************************************************************/
public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface>
{
public static final int DEFAULT_SORT_ORDER = 500;
/*******************************************************************************
** Produce the metaData object. Generally, you don't want to add it to the instance
@ -43,11 +46,13 @@ public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface>
/*******************************************************************************
** In case this producer needs to run before (or after) others, this method
** can help influence that (e.g., if used by MetaDataProducerHelper).
** can control influence that (e.g., if used by MetaDataProducerHelper).
**
** Smaller values run first.
*******************************************************************************/
public int getSortOrder()
{
return (500);
return (DEFAULT_SORT_ORDER);
}
}

View File

@ -37,17 +37,24 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
**
*******************************************************************************/
@JsonDeserialize(using = QBackendMetaDataDeserializer.class)
public class QBackendMetaData
public class QBackendMetaData implements TopLevelMetaDataInterface
{
private String name;
private String backendType;
private Boolean usesVariants = false;
private String variantOptionsTableName;
private Set<Capability> enabledCapabilities = new HashSet<>();
private Set<Capability> disabledCapabilities = new HashSet<>();
private Boolean usesVariants = false;
private String variantOptionsTableIdField;
private String variantOptionsTableNameField;
private String variantOptionsTableTypeField;
private String variantOptionsTableTypeValue;
private String variantOptionsTableUsernameField;
private String variantOptionsTablePasswordField;
private String variantOptionsTableApiKeyField;
private String variantOptionsTableName;
// todo - at some point, we may want to apply this to secret properties on subclasses?
// @JsonFilter("secretsFilter")
@ -398,7 +405,224 @@ public class QBackendMetaData
/*******************************************************************************
** Getter for variantsOptionTableName
** Getter for variantOptionsTableIdField
*******************************************************************************/
public String getVariantOptionsTableIdField()
{
return (this.variantOptionsTableIdField);
}
/*******************************************************************************
** Setter for variantOptionsTableIdField
*******************************************************************************/
public void setVariantOptionsTableIdField(String variantOptionsTableIdField)
{
this.variantOptionsTableIdField = variantOptionsTableIdField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableIdField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableIdField(String variantOptionsTableIdField)
{
this.variantOptionsTableIdField = variantOptionsTableIdField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableNameField
*******************************************************************************/
public String getVariantOptionsTableNameField()
{
return (this.variantOptionsTableNameField);
}
/*******************************************************************************
** Setter for variantOptionsTableNameField
*******************************************************************************/
public void setVariantOptionsTableNameField(String variantOptionsTableNameField)
{
this.variantOptionsTableNameField = variantOptionsTableNameField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableNameField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableNameField(String variantOptionsTableNameField)
{
this.variantOptionsTableNameField = variantOptionsTableNameField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableTypeField
*******************************************************************************/
public String getVariantOptionsTableTypeField()
{
return (this.variantOptionsTableTypeField);
}
/*******************************************************************************
** Setter for variantOptionsTableTypeField
*******************************************************************************/
public void setVariantOptionsTableTypeField(String variantOptionsTableTypeField)
{
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableTypeField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableTypeField(String variantOptionsTableTypeField)
{
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableTypeValue
*******************************************************************************/
public String getVariantOptionsTableTypeValue()
{
return (this.variantOptionsTableTypeValue);
}
/*******************************************************************************
** Setter for variantOptionsTableTypeValue
*******************************************************************************/
public void setVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
{
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableTypeValue
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
{
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableUsernameField
*******************************************************************************/
public String getVariantOptionsTableUsernameField()
{
return (this.variantOptionsTableUsernameField);
}
/*******************************************************************************
** Setter for variantOptionsTableUsernameField
*******************************************************************************/
public void setVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
{
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableUsernameField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
{
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTablePasswordField
*******************************************************************************/
public String getVariantOptionsTablePasswordField()
{
return (this.variantOptionsTablePasswordField);
}
/*******************************************************************************
** Setter for variantOptionsTablePasswordField
*******************************************************************************/
public void setVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
{
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTablePasswordField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
{
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableApiKeyField
*******************************************************************************/
public String getVariantOptionsTableApiKeyField()
{
return (this.variantOptionsTableApiKeyField);
}
/*******************************************************************************
** Setter for variantOptionsTableApiKeyField
*******************************************************************************/
public void setVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
{
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableApiKeyField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
{
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableName
*******************************************************************************/
public String getVariantOptionsTableName()
{
@ -408,7 +632,7 @@ public class QBackendMetaData
/*******************************************************************************
** Setter for variantsOptionTableName
** Setter for variantOptionsTableName
*******************************************************************************/
public void setVariantOptionsTableName(String variantOptionsTableName)
{
@ -418,12 +642,22 @@ public class QBackendMetaData
/*******************************************************************************
** Fluent setter for variantsOptionTableName
** Fluent setter for variantOptionsTableName
*******************************************************************************/
public QBackendMetaData withVariantsOptionTableName(String variantsOptionTableName)
public QBackendMetaData withVariantOptionsTableName(String variantOptionsTableName)
{
this.variantOptionsTableName = variantsOptionTableName;
this.variantOptionsTableName = variantOptionsTableName;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance qInstance)
{
qInstance.addBackend(this);
}
}

View File

@ -32,38 +32,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
*******************************************************************************/
public abstract class QSupplementalInstanceMetaData
{
protected String type;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QSupplementalInstanceMetaData withType(String type)
{
this.type = type;
return (this);
}
public abstract String getType();

View File

@ -22,9 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata.audits;
import java.util.List;
/*******************************************************************************
**
*******************************************************************************/
@ -32,9 +29,6 @@ public class QAuditRules
{
private AuditLevel auditLevel;
private boolean isAuditTreeRoot = false;
private List<String> auditTreeParentTableNames = null;
/*******************************************************************************
@ -77,66 +71,4 @@ public class QAuditRules
return (this);
}
/*******************************************************************************
** Getter for isAuditTreeRoot
*******************************************************************************/
public boolean getIsAuditTreeRoot()
{
return (this.isAuditTreeRoot);
}
/*******************************************************************************
** Setter for isAuditTreeRoot
*******************************************************************************/
public void setIsAuditTreeRoot(boolean isAuditTreeRoot)
{
this.isAuditTreeRoot = isAuditTreeRoot;
}
/*******************************************************************************
** Fluent setter for isAuditTreeRoot
*******************************************************************************/
public QAuditRules withIsAuditTreeRoot(boolean isAuditTreeRoot)
{
this.isAuditTreeRoot = isAuditTreeRoot;
return (this);
}
/*******************************************************************************
** Getter for auditTreeParentTableNames
*******************************************************************************/
public List<String> getAuditTreeParentTableNames()
{
return (this.auditTreeParentTableNames);
}
/*******************************************************************************
** Setter for auditTreeParentTableNames
*******************************************************************************/
public void setAuditTreeParentTableNames(List<String> auditTreeParentTableNames)
{
this.auditTreeParentTableNames = auditTreeParentTableNames;
}
/*******************************************************************************
** Fluent setter for auditTreeParentTableNames
*******************************************************************************/
public QAuditRules withAuditTreeParentTableNames(List<String> auditTreeParentTableNames)
{
this.auditTreeParentTableNames = auditTreeParentTableNames;
return (this);
}
}

View File

@ -28,8 +28,8 @@ import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.templates.RenderTemplateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateInput;
import com.kingsrook.qqq.backend.core.model.actions.templates.RenderTemplateOutput;
import com.kingsrook.qqq.backend.core.model.templates.TemplateType;

View File

@ -28,37 +28,10 @@ package com.kingsrook.qqq.backend.core.model.metadata.fields;
*******************************************************************************/
public abstract class QSupplementalFieldMetaData
{
protected String type;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QSupplementalFieldMetaData withType(String type)
{
this.type = type;
return (this);
}
public abstract String getType();
}

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.frontend;
import java.io.Serializable;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
@ -38,14 +39,15 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
@JsonInclude(Include.NON_NULL)
public class QFrontendFieldMetaData
{
private String name;
private String label;
private QFieldType type;
private boolean isRequired;
private boolean isEditable;
private boolean isHeavy;
private String possibleValueSourceName;
private String displayFormat;
private String name;
private String label;
private QFieldType type;
private boolean isRequired;
private boolean isEditable;
private boolean isHeavy;
private String possibleValueSourceName;
private String displayFormat;
private Serializable defaultValue;
private List<FieldAdornment> adornments;
@ -69,6 +71,7 @@ public class QFrontendFieldMetaData
this.possibleValueSourceName = fieldMetaData.getPossibleValueSourceName();
this.displayFormat = fieldMetaData.getDisplayFormat();
this.adornments = fieldMetaData.getAdornments();
this.defaultValue = fieldMetaData.getDefaultValue();
}
@ -170,4 +173,14 @@ public class QFrontendFieldMetaData
return possibleValueSourceName;
}
/*******************************************************************************
** Getter for defaultValue
**
*******************************************************************************/
public Serializable getDefaultValue()
{
return defaultValue;
}
}

View File

@ -27,6 +27,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude;
@ -41,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -57,21 +59,22 @@ public class QFrontendTableMetaData
private String label;
private boolean isHidden;
private String primaryKeyField;
private String iconName;
private String iconName;
private Map<String, QFrontendFieldMetaData> fields;
private List<QFieldSection> sections;
private List<QFrontendExposedJoin> exposedJoins;
private Set<String> capabilities;
private Map<String, QFrontendFieldMetaData> fields;
private List<QFieldSection> sections;
private List<QFrontendExposedJoin> exposedJoins;
private Map<String, QSupplementalTableMetaData> supplementalTableMetaData;
private Set<String> capabilities;
private boolean readPermission;
private boolean insertPermission;
private boolean editPermission;
private boolean deletePermission;
private boolean usesVariants;
private String variantTableLabel;
//////////////////////////////////////////////////////////////////////////////////
// do not add setters. take values from the source-object in the constructor!! //
//////////////////////////////////////////////////////////////////////////////////
@ -81,13 +84,13 @@ public class QFrontendTableMetaData
/*******************************************************************************
**
*******************************************************************************/
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields, boolean includeJoins)
public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins)
{
this.name = tableMetaData.getName();
this.label = tableMetaData.getLabel();
this.isHidden = tableMetaData.getIsHidden();
if(includeFields)
if(includeFullMetaData)
{
this.primaryKeyField = tableMetaData.getPrimaryKeyField();
this.fields = new HashMap<>();
@ -116,7 +119,7 @@ public class QFrontendTableMetaData
QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable());
frontendExposedJoin.setLabel(exposedJoin.getLabel());
frontendExposedJoin.setIsMany(exposedJoin.getIsMany());
frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFields, false));
frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFullMetaData, false));
for(String joinName : exposedJoin.getJoinPath())
{
frontendExposedJoin.addJoin(qInstance.getJoin(joinName));
@ -124,6 +127,28 @@ public class QFrontendTableMetaData
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// include supplemental meta data, based on if it's meant for full or partial frontend meta-data requests //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(tableMetaData.getSupplementalMetaData()).values())
{
boolean include;
if(includeFullMetaData)
{
include = supplementalTableMetaData.includeInFullFrontendMetaData();
}
else
{
include = supplementalTableMetaData.includeInPartialFrontendMetaData();
}
if(include)
{
this.supplementalTableMetaData = Objects.requireNonNullElseGet(this.supplementalTableMetaData, HashMap::new);
this.supplementalTableMetaData.put(supplementalTableMetaData.getType(), supplementalTableMetaData);
}
}
if(tableMetaData.getIcon() != null)
{
this.iconName = tableMetaData.getIcon().getName();
@ -135,6 +160,13 @@ public class QFrontendTableMetaData
insertPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.INSERT);
editPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.EDIT);
deletePermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.DELETE);
QBackendMetaData backend = actionInput.getInstance().getBackend(tableMetaData.getBackendName());
if(backend != null && backend.getUsesVariants())
{
usesVariants = true;
variantTableLabel = actionInput.getInstance().getTable(backend.getVariantOptionsTableName()).getLabel();
}
}
@ -294,6 +326,17 @@ public class QFrontendTableMetaData
/*******************************************************************************
** Getter for usesVariants
**
*******************************************************************************/
public boolean getUsesVariants()
{
return usesVariants;
}
/*******************************************************************************
** Getter for exposedJoins
**
@ -302,4 +345,26 @@ public class QFrontendTableMetaData
{
return exposedJoins;
}
/*******************************************************************************
** Getter for supplementalTableMetaData
**
*******************************************************************************/
public Map<String, QSupplementalTableMetaData> getSupplementalTableMetaData()
{
return supplementalTableMetaData;
}
/*******************************************************************************
** Getter for variantTableLabel
*******************************************************************************/
public String getVariantTableLabel()
{
return (this.variantTableLabel);
}
}

View File

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

View File

@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -36,7 +38,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
** MetaData definition of an App - an entity that organizes tables & processes
** and can be arranged hierarchically (e.g, apps can contain other apps).
*******************************************************************************/
public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRules
public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRules, TopLevelMetaDataInterface
{
private String name;
private String label;
@ -414,4 +416,14 @@ public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRu
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance qInstance)
{
qInstance.addApp(this);
}
}

View File

@ -33,38 +33,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
*******************************************************************************/
public abstract class QSupplementalProcessMetaData
{
protected String type;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QSupplementalProcessMetaData withType(String type)
{
this.type = type;
return (this);
}
public abstract String getType();

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.metadata.queues;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
@ -34,7 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaDa
** The processName is the code that runs for messages found on the queue.
** The schedule may not be used by all provider types, but defines when the queue is polled.
*******************************************************************************/
public class QQueueMetaData
public class QQueueMetaData implements TopLevelMetaDataInterface
{
private String name;
private String providerName;
@ -213,4 +215,15 @@ public class QQueueMetaData
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance qInstance)
{
qInstance.addQueue(this);
}
}

View File

@ -22,9 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata.scheduleing;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
/*******************************************************************************
** Meta-data to define scheduled actions within QQQ.
**
@ -46,11 +43,8 @@ public class QScheduleMetaData
private Integer initialDelaySeconds;
private Integer initialDelayMillis;
private RunStrategy variantRunStrategy;
private String backendVariant;
private String variantTableName;
private QQueryFilter variantFilter;
private String variantFieldName;
private RunStrategy variantRunStrategy;
private String variantBackend;
@ -191,124 +185,31 @@ public class QScheduleMetaData
/*******************************************************************************
** Getter for backendVariant
** Getter for variantBackend
*******************************************************************************/
public String getBackendVariant()
public String getVariantBackend()
{
return (this.backendVariant);
return (this.variantBackend);
}
/*******************************************************************************
** Setter for backendVariant
** Setter for variantBackend
*******************************************************************************/
public void setBackendVariant(String backendVariant)
public void setVariantBackend(String variantBackend)
{
this.backendVariant = backendVariant;
this.variantBackend = variantBackend;
}
/*******************************************************************************
** Fluent setter for backendVariant
** Fluent setter for variantBackend
*******************************************************************************/
public QScheduleMetaData withBackendVariant(String backendVariant)
{
this.backendVariant = backendVariant;
return (this);
}
/*******************************************************************************
** Getter for variantTableName
*******************************************************************************/
public String getVariantTableName()
{
return (this.variantTableName);
}
/*******************************************************************************
** Setter for variantTableName
*******************************************************************************/
public void setVariantTableName(String variantTableName)
{
this.variantTableName = variantTableName;
}
/*******************************************************************************
** Fluent setter for variantTableName
*******************************************************************************/
public QScheduleMetaData withVariantTableName(String variantTableName)
{
this.variantTableName = variantTableName;
return (this);
}
/*******************************************************************************
** Getter for variantFilter
*******************************************************************************/
public QQueryFilter getVariantFilter()
{
return (this.variantFilter);
}
/*******************************************************************************
** Setter for variantFilter
*******************************************************************************/
public void setVariantFilter(QQueryFilter variantFilter)
{
this.variantFilter = variantFilter;
}
/*******************************************************************************
** Fluent setter for variantFilter
*******************************************************************************/
public QScheduleMetaData withVariantFilter(QQueryFilter variantFilter)
{
this.variantFilter = variantFilter;
return (this);
}
/*******************************************************************************
** Getter for variantFieldName
*******************************************************************************/
public String getVariantFieldName()
{
return (this.variantFieldName);
}
/*******************************************************************************
** Setter for variantFieldName
*******************************************************************************/
public void setVariantFieldName(String variantFieldName)
{
this.variantFieldName = variantFieldName;
}
/*******************************************************************************
** Fluent setter for variantFieldName
*******************************************************************************/
public QScheduleMetaData withVariantFieldName(String variantFieldName)
{
this.variantFieldName = variantFieldName;
this.variantBackend = backendVariant;
return (this);
}

View File

@ -28,38 +28,32 @@ package com.kingsrook.qqq.backend.core.model.metadata.tables;
*******************************************************************************/
public abstract class QSupplementalTableMetaData
{
protected String type;
/*******************************************************************************
**
*******************************************************************************/
public boolean includeInPartialFrontendMetaData()
{
return (false);
}
/*******************************************************************************
**
*******************************************************************************/
public boolean includeInFullFrontendMetaData()
{
return (false);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QSupplementalTableMetaData withType(String type)
{
this.type = type;
return (this);
}
public abstract String getType();

View File

@ -49,6 +49,7 @@ public class CacheUseCase
//////////////////////////
private UniqueKey cacheUniqueKey;
private UniqueKey sourceUniqueKey;
private boolean doCopySourcePrimaryKeyToCache = false;
private List<QQueryFilter> excludeRecordsMatching;
@ -222,4 +223,35 @@ public class CacheUseCase
return (this);
}
/*******************************************************************************
** Getter for doCopySourcePrimaryKeyToCache
*******************************************************************************/
public boolean getDoCopySourcePrimaryKeyToCache()
{
return (this.doCopySourcePrimaryKeyToCache);
}
/*******************************************************************************
** Setter for doCopySourcePrimaryKeyToCache
*******************************************************************************/
public void setDoCopySourcePrimaryKeyToCache(boolean doCopySourcePrimaryKeyToCache)
{
this.doCopySourcePrimaryKeyToCache = doCopySourcePrimaryKeyToCache;
}
/*******************************************************************************
** Fluent setter for doCopySourcePrimaryKeyToCache
*******************************************************************************/
public CacheUseCase withDoCopySourcePrimaryKeyToCache(boolean doCopySourcePrimaryKeyToCache)
{
this.doCopySourcePrimaryKeyToCache = doCopySourcePrimaryKeyToCache;
return (this);
}
}

View File

@ -53,8 +53,8 @@ public class QueryStat extends QRecordEntity
@QField()
private Integer firstResultMillis;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String action;
@ -413,31 +413,31 @@ public class QueryStat extends QRecordEntity
/*******************************************************************************
** Getter for qqqTableId
** Getter for tableId
*******************************************************************************/
public Integer getQqqTableId()
public Integer getTableId()
{
return (this.qqqTableId);
return (this.tableId);
}
/*******************************************************************************
** Setter for qqqTableId
** Setter for tableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
public void setTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
** Fluent setter for tableId
*******************************************************************************/
public QueryStat withQqqTableId(Integer qqqTableId)
public QueryStat withTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
return (this);
}

View File

@ -42,8 +42,8 @@ public class QueryStatCriteriaField extends QRecordEntity
@QField(possibleValueSourceName = QueryStat.TABLE_NAME)
private Integer queryStatId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String name;
@ -138,31 +138,31 @@ public class QueryStatCriteriaField extends QRecordEntity
/*******************************************************************************
** Getter for qqqTableId
** Getter for tableId
*******************************************************************************/
public Integer getQqqTableId()
public Integer getTableId()
{
return (this.qqqTableId);
return (this.tableId);
}
/*******************************************************************************
** Setter for qqqTableId
** Setter for tableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
public void setTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
** Fluent setter for tableId
*******************************************************************************/
public QueryStatCriteriaField withQqqTableId(Integer qqqTableId)
public QueryStatCriteriaField withTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
return (this);
}

View File

@ -42,8 +42,8 @@ public class QueryStatJoinTable extends QRecordEntity
@QField(possibleValueSourceName = QueryStat.TABLE_NAME)
private Integer queryStatId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField(maxLength = 10, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String type;
@ -132,31 +132,31 @@ public class QueryStatJoinTable extends QRecordEntity
/*******************************************************************************
** Getter for qqqTableId
** Getter for tableId
*******************************************************************************/
public Integer getQqqTableId()
public Integer getTableId()
{
return (this.qqqTableId);
return (this.tableId);
}
/*******************************************************************************
** Setter for qqqTableId
** Setter for tableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
public void setTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
** Fluent setter for tableId
*******************************************************************************/
public QueryStatJoinTable withQqqTableId(Integer qqqTableId)
public QueryStatJoinTable withTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
return (this);
}

View File

@ -121,7 +121,7 @@ public class QueryStatMetaDataProvider
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withFieldsFromEntity(QueryStat.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "action", "qqqTableId", "sessionId")))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "action", "tableId", "sessionId")))
.withSection(new QFieldSection("data", new QIcon().withName("dataset"), Tier.T2, List.of("queryText", "startTimestamp", "firstResultTimestamp", "firstResultMillis")))
.withSection(new QFieldSection("joins", new QIcon().withName("merge"), Tier.T2).withWidgetName(joinTablesJoinName + "Widget"))
.withSection(new QFieldSection("criteria", new QIcon().withName("filter_alt"), Tier.T2).withWidgetName(criteriaFieldsJoinName + "Widget"))
@ -137,6 +137,10 @@ public class QueryStatMetaDataProvider
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(criteriaFieldsJoinName)).withName(criteriaFieldsJoinName + "Widget").withLabel("Criteria Fields").getWidgetMetaData());
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(orderByFieldsJoinName)).withName(orderByFieldsJoinName + "Widget").withLabel("Order by Fields").getWidgetMetaData());
table.withExposedJoin(new ExposedJoin().withJoinTable(QueryStatCriteriaField.TABLE_NAME));
table.withExposedJoin(new ExposedJoin().withJoinTable(QueryStatJoinTable.TABLE_NAME));
table.withExposedJoin(new ExposedJoin().withJoinTable(QueryStatOrderByField.TABLE_NAME));
table.withAssociation(new Association().withName("queryStatJoinTables").withJoinName(joinTablesJoinName).withAssociatedTableName(QueryStatJoinTable.TABLE_NAME))
.withAssociation(new Association().withName("queryStatCriteriaFields").withJoinName(criteriaFieldsJoinName).withAssociatedTableName(QueryStatCriteriaField.TABLE_NAME))
.withAssociation(new Association().withName("queryStatOrderByFields").withJoinName(orderByFieldsJoinName).withAssociatedTableName(QueryStatOrderByField.TABLE_NAME));

View File

@ -42,8 +42,8 @@ public class QueryStatOrderByField extends QRecordEntity
@QField(possibleValueSourceName = QueryStat.TABLE_NAME)
private Integer queryStatId;
@QField(label = "Table", possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer qqqTableId;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String name;
@ -132,31 +132,31 @@ public class QueryStatOrderByField extends QRecordEntity
/*******************************************************************************
** Getter for qqqTableId
** Getter for tableId
*******************************************************************************/
public Integer getQqqTableId()
public Integer getTableId()
{
return (this.qqqTableId);
return (this.tableId);
}
/*******************************************************************************
** Setter for qqqTableId
** Setter for tableId
*******************************************************************************/
public void setQqqTableId(Integer qqqTableId)
public void setTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for qqqTableId
** Fluent setter for tableId
*******************************************************************************/
public QueryStatOrderByField withQqqTableId(Integer qqqTableId)
public QueryStatOrderByField withTableId(Integer tableId)
{
this.qqqTableId = qqqTableId;
this.tableId = tableId;
return (this);
}

View File

@ -27,6 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
@ -48,8 +49,8 @@ public class SavedFilter extends QRecordEntity
@QField(isRequired = true)
private String label;
@QField(isEditable = false)
private String tableName;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField(isEditable = false)
private String userId;
@ -180,40 +181,6 @@ public class SavedFilter extends QRecordEntity
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Setter for tableName
**
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
**
*******************************************************************************/
public SavedFilter withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for userId
**
@ -280,4 +247,35 @@ public class SavedFilter extends QRecordEntity
return (this);
}
/*******************************************************************************
** Getter for tableId
*******************************************************************************/
public Integer getTableId()
{
return (this.tableId);
}
/*******************************************************************************
** Setter for tableId
*******************************************************************************/
public void setTableId(Integer tableId)
{
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for tableId
*******************************************************************************/
public SavedFilter withTableId(Integer tableId)
{
this.tableId = tableId;
return (this);
}
}

View File

@ -27,7 +27,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
/*******************************************************************************
@ -52,8 +52,8 @@ public class Script extends QRecordEntity
@QField(possibleValueSourceName = "scriptType")
private Integer scriptTypeId;
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName;
@QField(possibleValueSourceName = QQQTable.TABLE_NAME)
private Integer tableId;
@QField()
private Integer maxBatchSize;
@ -288,37 +288,6 @@ public class Script extends QRecordEntity
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public Script withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for maxBatchSize
*******************************************************************************/
@ -348,4 +317,35 @@ public class Script extends QRecordEntity
return (this);
}
/*******************************************************************************
** Getter for tableId
*******************************************************************************/
public Integer getTableId()
{
return (this.tableId);
}
/*******************************************************************************
** Setter for tableId
*******************************************************************************/
public void setTableId(Integer tableId)
{
this.tableId = tableId;
}
/*******************************************************************************
** Fluent setter for tableId
*******************************************************************************/
public Script withTableId(Integer tableId)
{
this.tableId = tableId;
return (this);
}
}

View File

@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.scripts;
import java.time.Instant;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QAssociation;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
@ -55,9 +57,6 @@ public class ScriptRevision extends QRecordEntity
@QField(possibleValueSourceName = "apiName", label = "API Name")
private String apiName;
@QField()
private String contents;
@QField()
private Integer sequenceNo;
@ -67,6 +66,9 @@ public class ScriptRevision extends QRecordEntity
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS)
private String author;
@QAssociation(name = "files")
private List<ScriptRevisionFile> files;
/*******************************************************************************
@ -226,40 +228,6 @@ public class ScriptRevision extends QRecordEntity
/*******************************************************************************
** Getter for contents
**
*******************************************************************************/
public String getContents()
{
return contents;
}
/*******************************************************************************
** Setter for contents
**
*******************************************************************************/
public void setContents(String contents)
{
this.contents = contents;
}
/*******************************************************************************
** Fluent setter for contents
**
*******************************************************************************/
public ScriptRevision withContents(String contents)
{
this.contents = contents;
return (this);
}
/*******************************************************************************
** Getter for sequenceNo
**
@ -422,4 +390,35 @@ public class ScriptRevision extends QRecordEntity
return (this);
}
/*******************************************************************************
** Getter for files
*******************************************************************************/
public List<ScriptRevisionFile> getFiles()
{
return (this.files);
}
/*******************************************************************************
** Setter for files
*******************************************************************************/
public void setFiles(List<ScriptRevisionFile> files)
{
this.files = files;
}
/*******************************************************************************
** Fluent setter for files
*******************************************************************************/
public ScriptRevision withFiles(List<ScriptRevisionFile> files)
{
this.files = files;
return (this);
}
}

View File

@ -0,0 +1,265 @@
/*
* 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.scripts;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
**
*******************************************************************************/
public class ScriptRevisionFile extends QRecordEntity
{
public static final String TABLE_NAME = "scriptRevisionFile";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = "scriptRevision")
private Integer scriptRevisionId;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String fileName;
@QField()
private String contents;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScriptRevisionFile()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScriptRevisionFile(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ScriptRevisionFile withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ScriptRevisionFile withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ScriptRevisionFile withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for scriptRevisionId
*******************************************************************************/
public Integer getScriptRevisionId()
{
return (this.scriptRevisionId);
}
/*******************************************************************************
** Setter for scriptRevisionId
*******************************************************************************/
public void setScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
}
/*******************************************************************************
** Fluent setter for scriptRevisionId
*******************************************************************************/
public ScriptRevisionFile withScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
return (this);
}
/*******************************************************************************
** Getter for fileName
*******************************************************************************/
public String getFileName()
{
return (this.fileName);
}
/*******************************************************************************
** Setter for fileName
*******************************************************************************/
public void setFileName(String fileName)
{
this.fileName = fileName;
}
/*******************************************************************************
** Fluent setter for fileName
*******************************************************************************/
public ScriptRevisionFile withFileName(String fileName)
{
this.fileName = fileName;
return (this);
}
/*******************************************************************************
** Getter for contents
*******************************************************************************/
public String getContents()
{
return (this.contents);
}
/*******************************************************************************
** Setter for contents
*******************************************************************************/
public void setContents(String contents)
{
this.contents = contents;
}
/*******************************************************************************
** Fluent setter for contents
*******************************************************************************/
public ScriptRevisionFile withContents(String contents)
{
this.contents = contents;
return (this);
}
}

View File

@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.scripts;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
@ -52,6 +53,33 @@ public class ScriptType extends QRecordEntity
@QField()
private String sampleCode;
@QField(possibleValueSourceName = ScriptTypeFileMode.NAME)
private Integer fileMode;
@QField()
private String testScriptInterfaceName;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScriptType()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScriptType(QRecord qRecord)
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
@ -256,4 +284,66 @@ public class ScriptType extends QRecordEntity
return (this);
}
/*******************************************************************************
** Getter for fileMode
*******************************************************************************/
public Integer getFileMode()
{
return (this.fileMode);
}
/*******************************************************************************
** Setter for fileMode
*******************************************************************************/
public void setFileMode(Integer fileMode)
{
this.fileMode = fileMode;
}
/*******************************************************************************
** Fluent setter for fileMode
*******************************************************************************/
public ScriptType withFileMode(Integer fileMode)
{
this.fileMode = fileMode;
return (this);
}
/*******************************************************************************
** Getter for testScriptInterfaceName
*******************************************************************************/
public String getTestScriptInterfaceName()
{
return (this.testScriptInterfaceName);
}
/*******************************************************************************
** Setter for testScriptInterfaceName
*******************************************************************************/
public void setTestScriptInterfaceName(String testScriptInterfaceName)
{
this.testScriptInterfaceName = testScriptInterfaceName;
}
/*******************************************************************************
** Fluent setter for testScriptInterfaceName
*******************************************************************************/
public ScriptType withTestScriptInterfaceName(String testScriptInterfaceName)
{
this.testScriptInterfaceName = testScriptInterfaceName;
return (this);
}
}

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.scripts;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PossibleValueEnum;
/*******************************************************************************
** ScriptTypeFileMode - possible value enum
*******************************************************************************/
public enum ScriptTypeFileMode implements PossibleValueEnum<Integer>
{
SINGLE(1, "Single File"),
MULTI_PRE_DEFINED(2, "Multi File (Pre-defined)"),
MULTI_AD_HOC(3, "Multi File (ad hoc)");
private final Integer id;
private final String label;
public static final String NAME = "scriptTypeFileMode";
/*******************************************************************************
**
*******************************************************************************/
ScriptTypeFileMode(Integer 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());
}
}

View File

@ -0,0 +1,265 @@
/*
* 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.scripts;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
/*******************************************************************************
**
*******************************************************************************/
public class ScriptTypeFileSchema extends QRecordEntity
{
public static final String TABLE_NAME = "scriptTypeFileSchema";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = "scriptType")
private Integer scriptTypeId;
@QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String name;
@QField(maxLength = 50, valueTooLongBehavior = ValueTooLongBehavior.ERROR)
private String fileType;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScriptTypeFileSchema()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public ScriptTypeFileSchema(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
*******************************************************************************/
public Integer getId()
{
return (this.id);
}
/*******************************************************************************
** Setter for id
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
*******************************************************************************/
public ScriptTypeFileSchema withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
*******************************************************************************/
public Instant getCreateDate()
{
return (this.createDate);
}
/*******************************************************************************
** Setter for createDate
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
*******************************************************************************/
public ScriptTypeFileSchema withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
*******************************************************************************/
public Instant getModifyDate()
{
return (this.modifyDate);
}
/*******************************************************************************
** Setter for modifyDate
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
*******************************************************************************/
public ScriptTypeFileSchema withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for scriptTypeId
*******************************************************************************/
public Integer getScriptTypeId()
{
return (this.scriptTypeId);
}
/*******************************************************************************
** Setter for scriptTypeId
*******************************************************************************/
public void setScriptTypeId(Integer scriptTypeId)
{
this.scriptTypeId = scriptTypeId;
}
/*******************************************************************************
** Fluent setter for scriptTypeId
*******************************************************************************/
public ScriptTypeFileSchema withScriptTypeId(Integer scriptTypeId)
{
this.scriptTypeId = scriptTypeId;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public ScriptTypeFileSchema withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for fileType
*******************************************************************************/
public String getFileType()
{
return (this.fileType);
}
/*******************************************************************************
** Setter for fileType
*******************************************************************************/
public void setFileType(String fileType)
{
this.fileType = fileType;
}
/*******************************************************************************
** Fluent setter for fileType
*******************************************************************************/
public ScriptTypeFileSchema withFileType(String fileType)
{
this.fileType = fileType;
return (this);
}
}

View File

@ -46,20 +46,27 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
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.QBackendStepMetaData;
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.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
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.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.LoadScriptTestDetailsProcessStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.RunRecordScriptExtractStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.RunRecordScriptLoadStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.RunRecordScriptPreStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.RunRecordScriptTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.StoreScriptRevisionProcessStep;
import com.kingsrook.qqq.backend.core.processes.implementations.scripts.TestScriptProcessStep;
@ -70,12 +77,15 @@ import com.kingsrook.qqq.backend.core.processes.implementations.scripts.TestScri
*******************************************************************************/
public class ScriptsMetaDataProvider
{
public static final String RUN_RECORD_SCRIPT_PROCESS_NAME = "runRecordScript";
public static final String STORE_SCRIPT_REVISION_PROCESS_NAME = "storeScriptRevision";
public static final String TEST_SCRIPT_PROCESS_NAME = "testScript";
public static final String RUN_RECORD_SCRIPT_PROCESS_NAME = "runRecordScript";
public static final String STORE_SCRIPT_REVISION_PROCESS_NAME = "storeScriptRevision";
public static final String TEST_SCRIPT_PROCESS_NAME = "testScript";
public static final String LOAD_SCRIPT_TEST_DETAILS_PROCESS_NAME = "loadScriptTestDetails";
public static final String SCRIPT_TYPE_NAME_RECORD = "Record Script";
public static final String CURRENT_SCRIPT_REVISION_JOIN_NAME = "currentScriptRevision";
/*******************************************************************************
@ -87,14 +97,37 @@ public class ScriptsMetaDataProvider
defineStandardScriptsPossibleValueSources(instance);
defineStandardScriptsJoins(instance);
defineStandardScriptsWidgets(instance);
// todo - change this from an enum-backed PVS to use qqqTable table, exposed in-app, in API
// so api docs don't always need refreshed
instance.addPossibleValueSource(TablesPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(instance));
instance.addProcess(defineStoreScriptRevisionProcess());
instance.addProcess(defineTestScriptProcess());
instance.addProcess(defineLoadScriptTestDetailsProcess());
instance.addProcess(defineRunRecordScriptProcess());
}
/*******************************************************************************
**
*******************************************************************************/
private QProcessMetaData defineLoadScriptTestDetailsProcess()
{
return (new QProcessMetaData()
.withName(LOAD_SCRIPT_TEST_DETAILS_PROCESS_NAME)
.withTableName(Script.TABLE_NAME)
.withIsHidden(true)
.withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED))
.withStepList(List.of(
new QBackendStepMetaData()
.withName("main")
.withCode(new QCodeReference(LoadScriptTestDetailsProcessStep.class)))));
}
/*******************************************************************************
**
*******************************************************************************/
@ -146,15 +179,25 @@ public class ScriptsMetaDataProvider
.withLoadStepClass(RunRecordScriptLoadStep.class)
.getProcessMetaData();
////////////////////////////////////////////////////////////////////////////
// add a screen before the extract step - where user selects their script //
////////////////////////////////////////////////////////////////////////////
processMetaData.addStep(0, new QFrontendStepMetaData()
.withName("input")
.withComponent(new QFrontendComponentMetaData().withType(QComponentType.EDIT_FORM))
.withFormField(new QFieldMetaData("scriptId", QFieldType.INTEGER).withPossibleValueSourceName(Script.TABLE_NAME)
.withPossibleValueSourceFilter(new QQueryFilter(
new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD),
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, "${input.tableName}")
new QFilterCriteria("tableId", QCriteriaOperator.EQUALS, "${input.tableId}")
))));
/////////////////////////////////////////////////////////////////////////////////
// now - insert a step before the input screen, where the table name gets read //
/////////////////////////////////////////////////////////////////////////////////
processMetaData.addStep(0, new QBackendStepMetaData()
.withName("preStep")
.withCode(new QCodeReference(RunRecordScriptPreStep.class)));
return (processMetaData);
}
@ -173,6 +216,14 @@ public class ScriptsMetaDataProvider
.withLabel("Recent Logs")
.getWidgetMetaData());
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(QJoinMetaData.makeInferredJoinName(ScriptType.TABLE_NAME, ScriptTypeFileSchema.TABLE_NAME)))
.withLabel("File Schema")
.getWidgetMetaData());
instance.addWidget(ChildRecordListRenderer.widgetMetaDataBuilder(instance.getJoin(QJoinMetaData.makeInferredJoinName(ScriptRevision.TABLE_NAME, ScriptRevisionFile.TABLE_NAME))).withMaxRows(50)
.withLabel("Files")
.getWidgetMetaData());
instance.addWidget(new QWidgetMetaData()
.withName("scriptViewer")
.withLabel("Contents")
@ -209,7 +260,7 @@ public class ScriptsMetaDataProvider
.withLeftTable(Script.TABLE_NAME)
.withRightTable(ScriptRevision.TABLE_NAME)
.withJoinOn(new JoinOn("currentScriptRevisionId", "id"))
.withName("currentScriptRevision"));
.withName(CURRENT_SCRIPT_REVISION_JOIN_NAME));
instance.addJoin(new QJoinMetaData()
.withType(JoinType.ONE_TO_MANY)
@ -227,6 +278,22 @@ public class ScriptsMetaDataProvider
.withOrderBy(new QFilterOrderBy("id"))
.withInferredName());
instance.addJoin(new QJoinMetaData()
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(ScriptType.TABLE_NAME)
.withRightTable(ScriptTypeFileSchema.TABLE_NAME)
.withJoinOn(new JoinOn("id", "scriptTypeId"))
.withOrderBy(new QFilterOrderBy("id"))
.withInferredName());
instance.addJoin(new QJoinMetaData()
.withType(JoinType.ONE_TO_MANY)
.withLeftTable(ScriptRevision.TABLE_NAME)
.withRightTable(ScriptRevisionFile.TABLE_NAME)
.withJoinOn(new JoinOn("id", "scriptRevisionId"))
.withOrderBy(new QFilterOrderBy("id"))
.withInferredName());
}
@ -251,23 +318,25 @@ public class ScriptsMetaDataProvider
{
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(Script.TABLE_NAME)
.withTableName(Script.TABLE_NAME)
);
.withTableName(Script.TABLE_NAME));
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(ScriptRevision.TABLE_NAME)
.withTableName(ScriptRevision.TABLE_NAME)
);
.withTableName(ScriptRevision.TABLE_NAME));
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(ScriptType.TABLE_NAME)
.withTableName(ScriptType.TABLE_NAME)
);
.withTableName(ScriptType.TABLE_NAME));
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(ScriptLog.TABLE_NAME)
.withTableName(ScriptLog.TABLE_NAME)
);
.withTableName(ScriptLog.TABLE_NAME));
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(ScriptTypeFileMode.NAME)
.withType(QPossibleValueSourceType.ENUM)
.withValuesFromEnum(ScriptTypeFileMode.values())
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY));
}
@ -279,8 +348,10 @@ public class ScriptsMetaDataProvider
{
List<QTableMetaData> rs = new ArrayList<>();
rs.add(enrich(backendDetailEnricher, defineScriptTypeTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScriptTypeFileSchemaTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScriptTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScriptRevisionTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScriptRevisionFileTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScriptLogTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineScriptLogLineTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineTableTriggerTable(backendName)));
@ -327,16 +398,16 @@ public class ScriptsMetaDataProvider
QTableMetaData tableMetaData = defineStandardTable(backendName, TableTrigger.TABLE_NAME, TableTrigger.class)
.withRecordLabelFields("id")
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id")))
.withSection(new QFieldSection("contents", new QIcon().withName("data_object"), Tier.T2, List.of("tableName", "filterId", "scriptId", "priority", "postInsert", "postUpdate")))
.withSection(new QFieldSection("contents", new QIcon().withName("data_object"), Tier.T2, List.of("tableId", "filterId", "scriptId", "priority", "postInsert", "postUpdate")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
tableMetaData.getField("scriptId").withPossibleValueSourceFilter(new QQueryFilter(
new QFilterCriteria("scriptType.name", QCriteriaOperator.EQUALS, SCRIPT_TYPE_NAME_RECORD),
new QFilterCriteria("script.tableName", QCriteriaOperator.EQUALS, "${input.tableName}")
new QFilterCriteria("script.tableId", QCriteriaOperator.EQUALS, "${input.tableId}")
));
tableMetaData.getField("filterId").withPossibleValueSourceFilter(new QQueryFilter(
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, "${input.tableName}")
new QFilterCriteria("tableId", QCriteriaOperator.EQUALS, "${input.tableId}")
));
return tableMetaData;
@ -351,7 +422,7 @@ public class ScriptsMetaDataProvider
{
QTableMetaData tableMetaData = defineStandardTable(backendName, Script.TABLE_NAME, Script.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name", "scriptTypeId", "currentScriptRevisionId")))
.withSection(new QFieldSection("recordScriptSettings", new QIcon().withName("table_rows"), Tier.T2, List.of("tableName", "maxBatchSize")))
.withSection(new QFieldSection("recordScriptSettings", new QIcon().withName("table_rows"), Tier.T2, List.of("tableId", "maxBatchSize")))
.withSection(new QFieldSection("contents", new QIcon().withName("data_object"), Tier.T2).withWidgetName("scriptViewer"))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
.withSection(new QFieldSection("lines", new QIcon().withName("horizontal_rule"), Tier.T2).withWidgetName(QJoinMetaData.makeInferredJoinName(Script.TABLE_NAME, ScriptLog.TABLE_NAME)));
@ -372,7 +443,8 @@ public class ScriptsMetaDataProvider
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptType.TABLE_NAME, ScriptType.class)
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name")))
.withSection(new QFieldSection("details", new QIcon().withName("dataset"), Tier.T2, List.of("helpText", "sampleCode")))
.withSection(new QFieldSection("details", new QIcon().withName("dataset"), Tier.T2, List.of("helpText", "sampleCode", "fileMode", "testScriptInterfaceName")))
.withSection(new QFieldSection("files", new QIcon().withName("description"), Tier.T2).withWidgetName(QJoinMetaData.makeInferredJoinName(ScriptType.TABLE_NAME, ScriptTypeFileSchema.TABLE_NAME)))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
tableMetaData.getField("sampleCode").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("javascript")));
tableMetaData.getField("helpText").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("text")));
@ -381,6 +453,22 @@ public class ScriptsMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineScriptTypeFileSchemaTable(String backendName) throws QException
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptTypeFileSchema.TABLE_NAME, ScriptTypeFileSchema.class)
.withRecordLabelFormat("%s - %s")
.withRecordLabelFields(List.of("scriptTypeId", "name"))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "name", "scriptTypeId")))
.withSection(new QFieldSection("details", new QIcon().withName("dataset"), Tier.T2, List.of("fileType")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
@ -391,11 +479,15 @@ public class ScriptsMetaDataProvider
.withRecordLabelFormat("%s v%s")
.withRecordLabelFields(List.of("scriptId", "sequenceNo"))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scriptId", "sequenceNo")))
.withSection(new QFieldSection("code", new QIcon().withName("data_object"), Tier.T2, List.of("contents")))
.withSection(new QFieldSection("files", new QIcon().withName("description"), Tier.T2).withWidgetName(QJoinMetaData.makeInferredJoinName(ScriptRevision.TABLE_NAME, ScriptRevisionFile.TABLE_NAME)))
.withSection(new QFieldSection("changeManagement", new QIcon().withName("history"), Tier.T2, List.of("commitMessage", "author")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")))
.withAssociation(new Association()
.withName("files")
.withAssociatedTableName(ScriptRevisionFile.TABLE_NAME)
.withJoinName(QJoinMetaData.makeInferredJoinName(ScriptRevision.TABLE_NAME, ScriptRevisionFile.TABLE_NAME)));
tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("javascript")));
tableMetaData.getField("scriptId").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment());
try
@ -420,6 +512,27 @@ public class ScriptsMetaDataProvider
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineScriptRevisionFileTable(String backendName) throws QException
{
QTableMetaData tableMetaData = defineStandardTable(backendName, ScriptRevisionFile.TABLE_NAME, ScriptRevisionFile.class)
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE)
.withRecordLabelFormat("%s - %s")
.withRecordLabelFields(List.of("scriptRevisionId", "fileName"))
.withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id", "scriptRevisionId", "fileName")))
.withSection(new QFieldSection("code", new QIcon().withName("data_object"), Tier.T2, List.of("contents")))
.withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate")));
tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("velocity"))); // todo - dynamic?!
tableMetaData.getField("scriptRevisionId").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment());
return (tableMetaData);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -0,0 +1,137 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.tables;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
/*******************************************************************************
** One-liner we can use to get a QQQTable record, or just its id (which we often want).
** Will insert the record if it wasn't already there.
** Also uses in-memory cache table, so rather cheap for normal use-case.
*******************************************************************************/
public class QQQTableAccessor
{
/*******************************************************************************
**
*******************************************************************************/
public static QRecord getQQQTableRecord(String tableName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", tableName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
QTableMetaData tableMetaData = QContext.getQInstance().getTable(tableName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord();
}
/*******************************************************************************
**
*******************************************************************************/
public static QRecord getQQQTableRecord(Integer id) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(QQQTable.TABLE_NAME);
sourceGetInput.setPrimaryKey(id);
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(sourceGetInput);
}
return getOutput.getRecord();
}
/*******************************************************************************
**
*******************************************************************************/
public static Integer getTableId(String tableName) throws QException
{
return (getQQQTableRecord(tableName).getValueInteger("id"));
}
/*******************************************************************************
**
*******************************************************************************/
public static String getTableName(Integer id) throws QException
{
return (getQQQTableRecord(id).getValueString("name"));
}
}

View File

@ -50,9 +50,20 @@ public class QQQTablesMetaDataProvider
*******************************************************************************/
public void defineAll(QInstance instance, String persistentBackendName, String cacheBackendName, Consumer<QTableMetaData> backendDetailEnricher) throws QException
{
instance.addTable(defineQQQTable(persistentBackendName, backendDetailEnricher));
instance.addTable(defineQQQTableCache(cacheBackendName, backendDetailEnricher));
instance.addPossibleValueSource(defineQQQTablePossibleValueSource());
if(instance.getTable(QQQTable.TABLE_NAME) == null)
{
instance.addTable(defineQQQTable(persistentBackendName, backendDetailEnricher));
}
if(instance.getTable(QQQ_TABLE_CACHE_TABLE_NAME) == null)
{
instance.addTable(defineQQQTableCache(cacheBackendName, backendDetailEnricher));
}
if(instance.getPossibleValueSource(QQQTable.TABLE_NAME) == null)
{
instance.addPossibleValueSource(defineQQQTablePossibleValueSource());
}
}
@ -105,6 +116,7 @@ public class QQQTablesMetaDataProvider
.withCacheSourceMisses(false)
.withCacheUniqueKey(new UniqueKey("name"))
.withSourceUniqueKey(new UniqueKey("name"))
.withDoCopySourcePrimaryKeyToCache(true)
)
);
@ -126,6 +138,7 @@ public class QQQTablesMetaDataProvider
return (new QPossibleValueSource()
.withType(QPossibleValueSourceType.TABLE)
.withName(QQQTable.TABLE_NAME)
.withOrderByField("label")
.withTableName(QQQTable.TABLE_NAME));
}

View File

@ -349,32 +349,37 @@ public class MemoryRecordStore
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
for(QRecord record : input.getRecords())
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make a copy of the record, to be inserted, and returned. this can avoid some cases where the in-memory store acts //
// differently from other backends, because of having the same record variable in the backend store and in the user-code. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord recordToInsert = new QRecord(record);
if(CollectionUtils.nullSafeHasContents(recordToInsert.getErrors()))
{
outputRecords.add(record);
outputRecords.add(recordToInsert);
continue;
}
/////////////////////////////////////////////////
// set the next serial in the record if needed //
/////////////////////////////////////////////////
if(record.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER))
if(recordToInsert.getValue(primaryKeyField.getName()) == null && primaryKeyField.getType().equals(QFieldType.INTEGER))
{
record.setValue(primaryKeyField.getName(), nextSerial++);
recordToInsert.setValue(primaryKeyField.getName(), nextSerial++);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// make sure that if the user supplied a serial, greater than the one we had, that we skip ahead //
///////////////////////////////////////////////////////////////////////////////////////////////////
if(primaryKeyField.getType().equals(QFieldType.INTEGER) && record.getValueInteger(primaryKeyField.getName()) > nextSerial)
if(primaryKeyField.getType().equals(QFieldType.INTEGER) && recordToInsert.getValueInteger(primaryKeyField.getName()) > nextSerial)
{
nextSerial = record.getValueInteger(primaryKeyField.getName()) + 1;
nextSerial = recordToInsert.getValueInteger(primaryKeyField.getName()) + 1;
}
tableData.put(record.getValue(primaryKeyField.getName()), record);
tableData.put(recordToInsert.getValue(primaryKeyField.getName()), recordToInsert);
if(returnInsertedRecords)
{
outputRecords.add(record);
outputRecords.add(recordToInsert);
}
}

View File

@ -1,199 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.audits;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.gson.reflect.TypeToken;
import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.TableMetaDataOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
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.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.audits.AuditsMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** This is a single-step process used to look up audits for a record.
*******************************************************************************/
public class GetRecordAuditsStep implements BackendStep
{
private static final QLogger LOG = QLogger.getLogger(GetRecordAuditsStep.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
try
{
String tableName = runBackendStepInput.getValueString("tableName");
Integer recordId = runBackendStepInput.getValueInteger("recordId");
String sortDirection = runBackendStepInput.getValueString("sortDirection");
Integer limit = 1000;
/////////////////////////////////////////
// make sure user may query this table //
/////////////////////////////////////////
PermissionsHelper.checkTablePermissionThrowing(new QueryInput().withTableName(tableName), TablePermissionSubType.READ);
PermissionsHelper.checkTablePermissionThrowing(new QueryInput().withTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT), TablePermissionSubType.READ);
/////////////////////////////////////////////////////////////////////////
// set up filter for audits - always start with the record in question //
// possibly add in other pairs of table/recordId based on the tree... //
// combine all of those options in a filter of OR's //
/////////////////////////////////////////////////////////////////////////
QQueryFilter auditRecordFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
auditRecordFilter.addSubFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE + ".name", QCriteriaOperator.EQUALS, tableName))
.withCriteria(new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, recordId)));
Set<Integer> otherAuditTableIds = new HashSet<>();
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the table is part of an audit tree, then find other table/recordId pairs to look for in the audits query //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = QContext.getQInstance().getTable(tableName);
QAuditRules auditRules = Objects.requireNonNullElseGet(table.getAuditRules(), QAuditRules::new);
if(auditRules.getIsAuditTreeRoot())
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TREE);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("rootTable.name", QCriteriaOperator.EQUALS, tableName)));
queryInput.withQueryJoin(new QueryJoin(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE)
.withJoinMetaData(QContext.getQInstance().getJoin(AuditsMetaDataProvider.AUDIT_TREE_JOIN_AUDIT_TABLE_FOR_ROOT))
.withAlias("rootTable"));
for(QRecord auditTreeRecord : new QueryAction().execute(queryInput).getRecords())
{
otherAuditTableIds.add(auditTreeRecord.getValueInteger("nodeAuditTableId"));
auditRecordFilter.addSubFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("auditTableId", QCriteriaOperator.EQUALS, auditTreeRecord.getValue("nodeAuditTableId")))
.withCriteria(new QFilterCriteria("recordId", QCriteriaOperator.EQUALS, auditTreeRecord.getValue("nodeRecordId"))));
}
}
////////////////////////////////////////////////
// todo - reverse (e.g., for child, non-root) //
////////////////////////////////////////////////
///////////////////////
// select the audits //
///////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setIncludeAssociations(true);
queryInput.setFilter(new QQueryFilter()
.withLimit(limit)
.withSubFilters(List.of(auditRecordFilter))
.withOrderBy(new QFilterOrderBy("timestamp", "asc".equals(sortDirection)))
);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
ArrayList<QRecord> audits = CollectionUtils.useOrWrap(queryOutput.getRecords(), new TypeToken<>() {}); // todo - share simple-class format change, verify in checkstyle
runBackendStepOutput.addValue("audits", audits);
///////////////////////////////////////////////////////
// look up count if needed, else use audit list size //
///////////////////////////////////////////////////////
if(audits.size() >= limit)
{
CountInput countInput = new CountInput();
countInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT);
countInput.setFilter(new QQueryFilter().withSubFilters(List.of(auditRecordFilter)));
CountOutput countOutput = new CountAction().execute(countInput);
runBackendStepOutput.addValue("count", countOutput.getCount());
}
else
{
runBackendStepOutput.addValue("count", audits.size());
}
////////////////////////////////////////////////////////////////////////////////////////////////
// put map of auditTableId to table names, and table name to (full) table meta data in result //
////////////////////////////////////////////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(otherAuditTableIds))
{
QueryInput auditTableQueryInput = new QueryInput();
auditTableQueryInput.setTableName(AuditsMetaDataProvider.TABLE_NAME_AUDIT_TABLE);
auditTableQueryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.IN, new ArrayList<>(otherAuditTableIds))));
QueryOutput auditTableQueryOutput = new QueryAction().execute(auditTableQueryInput);
HashMap<Integer, QRecord> auditTableMap = auditTableQueryOutput.getRecords().stream().collect(Collectors.toMap(r -> r.getValueInteger("id"), r -> r, (a, b) -> a, HashMap::new));
runBackendStepOutput.addValue("auditTableMap", auditTableMap);
HashMap<String, QFrontendTableMetaData> tableMetaDataMap = new HashMap<>();
for(QRecord auditTable : auditTableMap.values())
{
String subTableName = auditTable.getValueString("name");
TableMetaDataInput tableMetaDataInput = new TableMetaDataInput();
tableMetaDataInput.setTableName(subTableName);
TableMetaDataOutput tableMetaDataOutput = new TableMetaDataAction().execute(tableMetaDataInput);
tableMetaDataMap.put(subTableName, tableMetaDataOutput.getTable());
}
runBackendStepOutput.addValue("tableMetaDataMap", tableMetaDataMap);
}
}
catch(Exception e)
{
throw new QException("Error getting record audits", e);
}
}
}

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.processes.implementations.columnstats;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
@ -303,14 +305,18 @@ public class ColumnStatsStep implements BackendStep
/////////////////////////////////////////////////////////////////////////////////
// just in case any of these don't fit in an integer, use decimal for them all //
/////////////////////////////////////////////////////////////////////////////////
Aggregate countNonNullAggregate = new Aggregate(fieldName, AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL);
Aggregate countDistinctAggregate = new Aggregate(fieldName, AggregateOperator.COUNT_DISTINCT).withFieldType(QFieldType.DECIMAL);
Aggregate sumAggregate = new Aggregate(fieldName, AggregateOperator.SUM).withFieldType(QFieldType.DECIMAL);
Aggregate avgAggregate = new Aggregate(fieldName, AggregateOperator.AVG).withFieldType(QFieldType.DECIMAL);
Aggregate minAggregate = new Aggregate(fieldName, AggregateOperator.MIN);
Aggregate maxAggregate = new Aggregate(fieldName, AggregateOperator.MAX);
AggregateInput statsAggregateInput = new AggregateInput();
Aggregate countTotalRowsAggregate = new Aggregate(table.getPrimaryKeyField(), AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL);
Aggregate countNonNullAggregate = new Aggregate(fieldName, AggregateOperator.COUNT).withFieldType(QFieldType.DECIMAL);
Aggregate countDistinctAggregate = new Aggregate(fieldName, AggregateOperator.COUNT_DISTINCT).withFieldType(QFieldType.DECIMAL);
Aggregate sumAggregate = new Aggregate(fieldName, AggregateOperator.SUM).withFieldType(QFieldType.DECIMAL);
Aggregate avgAggregate = new Aggregate(fieldName, AggregateOperator.AVG).withFieldType(QFieldType.DECIMAL);
Aggregate minAggregate = new Aggregate(fieldName, AggregateOperator.MIN);
Aggregate maxAggregate = new Aggregate(fieldName, AggregateOperator.MAX);
AggregateInput statsAggregateInput = new AggregateInput();
statsAggregateInput.withAggregate(countTotalRowsAggregate);
statsAggregateInput.withAggregate(countNonNullAggregate);
if(doCountDistinct)
{
statsAggregateInput.withAggregate(countDistinctAggregate);
@ -332,6 +338,7 @@ public class ColumnStatsStep implements BackendStep
statsAggregateInput.withAggregate(maxAggregate);
}
BigDecimal totalRows = null;
if(CollectionUtils.nullSafeHasContents(statsAggregateInput.getAggregates()))
{
statsAggregateInput.setTableName(tableName);
@ -346,6 +353,8 @@ public class ColumnStatsStep implements BackendStep
{
AggregateResult statsAggregateResult = statsAggregateOutput.getResults().get(0);
totalRows = ValueUtils.getValueAsBigDecimal(statsAggregateResult.getAggregateValue(countTotalRowsAggregate));
statsRecord.setValue(countNonNullField.getName(), statsAggregateResult.getAggregateValue(countNonNullAggregate));
if(doCountDistinct)
{
@ -388,6 +397,27 @@ public class ColumnStatsStep implements BackendStep
}
}
/////////////////////
// figure count%'s //
/////////////////////
if(totalRows == null)
{
totalRows = new BigDecimal(valueCounts.stream().mapToInt(r -> r.getValueInteger("count")).sum());
}
if(totalRows != null && totalRows.compareTo(BigDecimal.ZERO) > 0)
{
BigDecimal oneHundred = new BigDecimal(100);
for(QRecord valueCount : valueCounts)
{
BigDecimal percent = new BigDecimal(Objects.requireNonNullElse(valueCount.getValueInteger("count"), 0)).divide(totalRows, 4, RoundingMode.HALF_UP).multiply(oneHundred).setScale(2, RoundingMode.HALF_UP);
valueCount.setValue("percent", percent);
}
QFieldMetaData percentField = new QFieldMetaData("percent", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.PERCENT_POINT2).withLabel("Percent");
QValueFormatter.setDisplayValuesInRecords(Map.of(fieldName, field, "percent", percentField), valueCounts);
}
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(null);
fields.forEach(qInstanceEnricher::enrichField);

View File

@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
/*******************************************************************************
@ -100,7 +101,7 @@ public class QuerySavedFilterProcess implements BackendStep
QueryInput input = new QueryInput();
input.setTableName(SavedFilter.TABLE_NAME);
input.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, tableName))
.withCriteria(new QFilterCriteria("tableId", QCriteriaOperator.EQUALS, QQQTableAccessor.getTableId(tableName)))
.withOrderBy(new QFilterOrderBy("label")));
QueryOutput output = new QueryAction().execute(input);

View File

@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
/*******************************************************************************
@ -82,7 +83,7 @@ public class StoreSavedFilterProcess implements BackendStep
QRecord qRecord = new QRecord()
.withValue("id", runBackendStepInput.getValueInteger("id"))
.withValue("label", runBackendStepInput.getValueString("label"))
.withValue("tableName", runBackendStepInput.getValueString("tableName"))
.withValue("tableId", QQQTableAccessor.getTableId(runBackendStepInput.getValueString("tableName")))
.withValue("filterJson", runBackendStepInput.getValueString("filterJson"))
.withValue("userId", runBackendStepInput.getSession().getUser().getIdReference());

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.scripts;
import java.util.ArrayList;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptType;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Action to load the details necessary to test a script.
**
*******************************************************************************/
public class LoadScriptTestDetailsProcessStep implements BackendStep
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
try
{
ActionHelper.validateSession(input);
Integer scriptTypeId = input.getValueInteger("scriptTypeId");
GetInput getInput = new GetInput();
getInput.setTableName(ScriptType.TABLE_NAME);
getInput.setPrimaryKey(scriptTypeId);
GetOutput getOutput = new GetAction().execute(getInput);
ScriptType scriptType = new ScriptType(getOutput.getRecord());
TestScriptActionInterface testScriptActionInterface = QCodeLoader.getAdHoc(TestScriptActionInterface.class, new QCodeReference(scriptType.getTestScriptInterfaceName(), QCodeType.JAVA));
QInstanceEnricher qInstanceEnricher = new QInstanceEnricher(new QInstance());
ArrayList<QFieldMetaData> inputFields = new ArrayList<>();
for(QFieldMetaData testInputField : CollectionUtils.nonNullList(testScriptActionInterface.getTestInputFields()))
{
qInstanceEnricher.enrichField(testInputField);
inputFields.add(testInputField);
}
ArrayList<QFieldMetaData> outputFields = new ArrayList<>();
for(QFieldMetaData testOutputField : CollectionUtils.nonNullList(testScriptActionInterface.getTestOutputFields()))
{
qInstanceEnricher.enrichField(testOutputField);
outputFields.add(testOutputField);
}
output.addValue("testInputFields", inputFields);
output.addValue("testOutputFields", outputFields);
}
catch(Exception e)
{
output.addValue("exception", e);
}
}
}

View File

@ -30,6 +30,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -58,6 +59,11 @@ public class RunRecordScriptExtractStep extends ExtractViaQueryStep
runBackendStepInput.addValue(FIELD_SOURCE_TABLE, tableName);
/////////////////////////////////////////////////////////////////
// set this value, for the select-script possible-value filter //
/////////////////////////////////////////////////////////////////
runBackendStepInput.addValue("tableId", QQQTableAccessor.getTableId(tableName));
Integer scriptId = runBackendStepInput.getValueInteger("scriptId");
GetInput getInput = new GetInput();
getInput.setTableName(Script.TABLE_NAME);

View File

@ -0,0 +1,64 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.processes.implementations.scripts;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.tables.QQQTableAccessor;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** pre-step for the run-record-script process. Help deal with this being
** a generic process (e.g., no table name defined in the meta data).
*******************************************************************************/
public class RunRecordScriptPreStep implements BackendStep
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is a generic (e.g., not table-specific) process - so we must be sure to set the tableName field in the expected slot. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String tableName = runBackendStepInput.getValueString("tableName");
if(!StringUtils.hasContent(tableName))
{
throw (new QException("Table name was not specified as input value"));
}
runBackendStepInput.addValue(StreamedETLProcess.FIELD_SOURCE_TABLE, tableName);
/////////////////////////////////////////////////////////////////
// set this value, for the select-script possible-value filter //
/////////////////////////////////////////////////////////////////
runBackendStepInput.addValue("tableId", QQQTableAccessor.getTableId(tableName));
}
}

View File

@ -22,8 +22,10 @@
package com.kingsrook.qqq.backend.core.processes.implementations.scripts;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
@ -44,6 +46,8 @@ 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.scripts.ScriptRevisionFile;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -63,87 +67,142 @@ public class StoreScriptRevisionProcessStep implements BackendStep
@Override
public void run(RunBackendStepInput input, RunBackendStepOutput output) throws QException
{
ActionHelper.validateSession(input);
//////////////////////////////////////////////////////////////////
// check if there's currently a script referenced by the record //
//////////////////////////////////////////////////////////////////
Integer scriptId = input.getValueInteger("scriptId");
Integer nextSequenceNo = 1;
////////////////////////////////////////
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(scriptId);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord script = getOutput.getRecord();
QueryInput queryInput = new QueryInput();
queryInput.setTableName("scriptRevision");
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
.withLimit(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.getValueString("commitMessage");
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.getValueString("contents"))
.withValue("apiName", input.getValueString("apiName"))
.withValue("apiVersion", input.getValueString("apiVersion"))
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);
InsertAction insertAction = new InsertAction();
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
QBackendTransaction transaction = insertAction.openTransaction(insertInput);
insertInput.setTransaction(transaction);
try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
ActionHelper.validateSession(input);
//////////////////////////////////////////////////////////////////
// check if there's currently a script referenced by the record //
//////////////////////////////////////////////////////////////////
Integer scriptId = input.getValueInteger("scriptId");
Integer nextSequenceNo = 1;
////////////////////////////////////////
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(scriptId);
getInput.setTransaction(transaction);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord script = getOutput.getRecord();
QueryInput queryInput = new QueryInput();
queryInput.setTableName("scriptRevision");
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
.withLimit(1));
queryInput.setTransaction(transaction);
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.getValueString("commitMessage");
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("apiName", input.getValueString("apiName"))
.withValue("apiVersion", input.getValueString("apiVersion"))
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);
try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
}
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = insertAction.execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
Integer scriptRevisionId = scriptRevision.getValueInteger("id");
//////////////////////////////////////////
// Store the file(s) under the revision //
//////////////////////////////////////////
List<QRecord> scriptRevisionFileRecords = null;
if(StringUtils.hasContent(input.getValueString("fileNames")))
{
scriptRevisionFileRecords = new ArrayList<>();
for(String fileName : input.getValueString("fileNames").split(","))
{
scriptRevisionFileRecords.add(new ScriptRevisionFile()
.withScriptRevisionId(scriptRevisionId)
.withFileName(fileName)
.withContents(input.getValueString("fileContents:" + fileName))
.toQRecord());
}
}
else if(StringUtils.hasContent(input.getValueString("contents")))
{
scriptRevisionFileRecords = new ArrayList<>();
scriptRevisionFileRecords.add(new ScriptRevisionFile()
.withScriptRevisionId(scriptRevisionId)
.withFileName("Script.js")
.withContents(input.getValueString("contents"))
.toQRecord());
}
if(CollectionUtils.nullSafeHasContents(scriptRevisionFileRecords))
{
InsertInput scriptRevisionFileInsertInput = new InsertInput();
scriptRevisionFileInsertInput.setTableName(ScriptRevisionFile.TABLE_NAME);
scriptRevisionFileInsertInput.setRecords(scriptRevisionFileRecords);
scriptRevisionFileInsertInput.setTransaction(transaction);
new InsertAction().execute(scriptRevisionFileInsertInput);
}
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
updateInput.setTransaction(transaction);
new UpdateAction().execute(updateInput);
transaction.commit();
output.addValue("scriptId", script.getValueInteger("id"));
output.addValue("scriptName", script.getValueString("name"));
output.addValue("scriptRevisionId", scriptRevisionId);
output.addValue("scriptRevisionSequenceNo", scriptRevision.getValueInteger("sequenceNo"));
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
transaction.rollback();
}
finally
{
transaction.close();
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);
output.addValue("scriptId", script.getValueInteger("id"));
output.addValue("scriptName", script.getValueString("name"));
output.addValue("scriptRevisionId", scriptRevision.getValueInteger("id"));
output.addValue("scriptRevisionSequenceNo", scriptRevision.getValueInteger("sequenceNo"));
}
}

View File

@ -22,35 +22,34 @@
package com.kingsrook.qqq.backend.core.processes.implementations.scripts;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.reflect.TypeToken;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.scripts.RunAdHocRecordScriptAction;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.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.model.metadata.code.AdHocScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
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;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptType;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -70,69 +69,84 @@ public class TestScriptProcessStep implements BackendStep
{
ActionHelper.validateSession(input);
////////////////
// get inputs //
////////////////
///////////////////////////////////////////////////////////////////////
// build a script revision based on the input params & file contents //
///////////////////////////////////////////////////////////////////////
Integer scriptId = input.getValueInteger("scriptId");
ScriptRevision scriptRevision = new ScriptRevision();
scriptRevision.setScriptId(scriptId);
scriptRevision.setContents(input.getValueString("code"));
ArrayList<ScriptRevisionFile> files = new ArrayList<>();
if(StringUtils.hasContent(input.getValueString("fileNames")))
{
for(String fileName : input.getValueString("fileNames").split(","))
{
files.add(new ScriptRevisionFile()
.withFileName(fileName)
.withContents(input.getValueString("fileContents:" + fileName)));
}
}
scriptRevision.setFiles(files);
scriptRevision.setApiName(input.getValueString("apiName"));
scriptRevision.setApiVersion(input.getValueString("apiVersion"));
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
///////////////////////////////////////////////////////
// set up a code reference using the script revision //
///////////////////////////////////////////////////////
AdHocScriptCodeReference adHocScriptCodeReference = new AdHocScriptCodeReference().withScriptRevisionRecord(scriptRevision.toQRecord());
adHocScriptCodeReference.setCodeType(QCodeType.JAVA_SCRIPT); // todo - load dynamically?
adHocScriptCodeReference.setInlineCode(scriptRevision.getFiles().get(0).getContents()); // todo - ugh.
/////////////////////////////////////////////////////////////////
// lookup the script - figure out how to proceed based on type //
/////////////////////////////////////////////////////////////////
QRecord script = getScript(scriptId);
String scriptTypeName = getScriptTypeName(script);
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// load the script and its type, to find the TestScriptActionInterface where the script will be tested //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
QRecord script = getScript(scriptId);
Integer scriptTypeId = script.getValueInteger("scriptTypeId");
GetInput getInput = new GetInput();
getInput.setTableName(ScriptType.TABLE_NAME);
getInput.setPrimaryKey(scriptTypeId);
GetOutput getOutput = new GetAction().execute(getInput);
ScriptType scriptType = new ScriptType(getOutput.getRecord());
if(ScriptsMetaDataProvider.SCRIPT_TYPE_NAME_RECORD.equals(scriptTypeName))
TestScriptActionInterface testScriptActionInterface = QCodeLoader.getAdHoc(TestScriptActionInterface.class, new QCodeReference(scriptType.getTestScriptInterfaceName(), QCodeType.JAVA));
/////////////////////////////////////////////////////////////////////////////////////////////////
// finish setting up input for the testScript action - including coyping over all input values //
/////////////////////////////////////////////////////////////////////////////////////////////////
TestScriptInput testScriptInput = new TestScriptInput();
testScriptInput.setApiName(input.getValueString("apiName"));
testScriptInput.setApiVersion(input.getValueString("apiVersion"));
testScriptInput.setCodeReference(adHocScriptCodeReference);
Map<String, Serializable> inputValues = new HashMap<>();
testScriptInput.setInputValues(inputValues);
for(Map.Entry<String, Serializable> entry : input.getValues().entrySet())
{
String tableName = script.getValueString("tableName");
QTableMetaData table = QContext.getQInstance().getTable(tableName);
if(table == null)
{
throw (new QException("Could not find table [" + tableName + "] for script"));
}
String recordPrimaryKeyList = input.getValueString("recordPrimaryKeyList");
if(!StringUtils.hasContent(recordPrimaryKeyList))
{
throw (new QException("Record primary key list was not given."));
}
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, recordPrimaryKeyList.split(","))));
queryInput.setIncludeAssociations(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{
throw (new QException("No records were found by the given primary keys."));
}
RunAdHocRecordScriptInput runAdHocRecordScriptInput = new RunAdHocRecordScriptInput();
runAdHocRecordScriptInput.setRecordList(queryOutput.getRecords());
runAdHocRecordScriptInput.setLogger(executionLogger);
runAdHocRecordScriptInput.setTableName(tableName);
runAdHocRecordScriptInput.setCodeReference(new AdHocScriptCodeReference().withScriptRevisionRecord(scriptRevision.toQRecord()));
RunAdHocRecordScriptOutput runAdHocRecordScriptOutput = new RunAdHocRecordScriptOutput();
new RunAdHocRecordScriptAction().run(runAdHocRecordScriptInput, runAdHocRecordScriptOutput);
/////////////////////////////////////////////
// if there was an exception, send it back //
/////////////////////////////////////////////
runAdHocRecordScriptOutput.getException().ifPresent(e -> output.addValue("exception", e));
}
else
{
throw new QException("This process does not know how to test a script of type: " + scriptTypeName);
String key = entry.getKey();
String value = ValueUtils.getValueAsString(entry.getValue());
inputValues.put(key, value);
}
output.addValue("scriptLogLines", new ArrayList<>(executionLogger.getScriptLogLines()));
////////////////////////////////
// run the test script action //
////////////////////////////////
TestScriptOutput testScriptOutput = new TestScriptOutput();
testScriptActionInterface.execute(testScriptInput, testScriptOutput);
//////////////////////////////////
// send script outputs back out //
//////////////////////////////////
output.addValue("scriptLogLines", CollectionUtils.useOrWrap(testScriptOutput.getScriptLogLines(), TypeToken.get(ArrayList.class)));
output.addValue("outputObject", testScriptOutput.getOutputObject());
if(testScriptOutput.getException() != null)
{
output.addValue("exception", testScriptOutput.getException());
output.setException(testScriptOutput.getException());
}
}
catch(Exception e)
{
@ -140,6 +154,7 @@ public class TestScriptProcessStep implements BackendStep
// is this the kind of exception meant here? or is it more for one thrown by the script execution? or are those the same?? //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
output.addValue("exception", e);
output.setException(e);
}
}

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