Compare commits

...

223 Commits

Author SHA1 Message Date
d977abfc3d i guess methods starting with underscores are not allowed 2023-09-25 18:33:16 -05:00
c559cc21eb i guess methods starting with underscores are not allowed 2023-09-25 18:29:39 -05:00
8ebe776847 added json utility method for sorting a json 'thing' 2023-09-25 18:25:11 -05:00
eefbdd212f Merge pull request #42 from Kingsrook/feature/CE-609-infrastructure-remove-permissions-from-header
Feature/ce 609 infrastructure remove permissions from header
2023-09-25 16:01:46 -05:00
9e9d2926c6 Nicer user-facting exceptions for throwUnsupportedCriteriaOperator and throwUnsupportedCriteriaField 2023-09-25 14:54:25 -05:00
994ab15652 Remove unused fields 2023-09-25 14:09:55 -05:00
164087beb0 Merge pull request #41 from Kingsrook/integration/20230921
automationStatus → OK fixes; script + audit updates;
2023-09-25 13:24:44 -05:00
070dec1266 Merge branch 'feature/script-audit-and-audit-change-cleanup' into integration/20230921 2023-09-21 15:04:00 -05:00
27c9694433 Merge branch 'feature/automation-status-fixes' into integration/20230921 2023-09-21 15:03:33 -05:00
a95e9d06a2 Add shortRepo name for Infoplus-Scripts... 2023-09-21 14:55:28 -05:00
b9b32d4b7d Add option to (poorly) format SQL for logs 2023-09-21 14:54:54 -05:00
1c99ea2c6f Build audits when running Record Scripts; add script name to audit context; clean up some bogus 'changed x to x' messages. 2023-09-21 14:42:01 -05:00
f19cd26892 Fix canWeSkipPendingAndGoToOkay to only ever return true if its input status is a Pending status. 2023-09-21 13:45:04 -05:00
253e93c356 Merge pull request #40 from Kingsrook/dev
refreshign CE-609 with dev
2023-09-14 14:37:47 -05:00
3e8afde744 Merge pull request #39 from Kingsrook/feature/instance-and-script-deployment-mode
Add deploymentMode as a field in QInstance; pass it into scripts (e.g…
2023-09-14 14:13:43 -05:00
ce823ad22f Add deploymentMode as a field in QInstance; pass it into scripts (e.g., in executeCodeAction) 2023-09-14 12:16:58 -05:00
93e1c01939 Fixed getIntegerFromPropertyOrEnvironment, when it gets a value from env (was parsing the prop value instead); added tests on getIntegerFromPropertyOrEnvironment 2023-09-08 10:58:38 -05:00
c37eead6be Add a reasonable order-by to all table-based PossibleValueSources defined in QQQ; fix using order-by in SearchPossibleValueSourceAction 2023-09-08 10:53:31 -05:00
d9458ced34 Add LOG.warns any time we rollback a transaction from top-level StreamedETL process code. 2023-09-07 12:08:43 -05:00
01a19180b9 Fix some cases of joins in exports w/ multiple possible paths 2023-08-18 16:25:00 -05:00
406069768b Updating shortRepo values 2023-08-17 15:48:48 -05:00
83055e1784 Merge branch 'dev' into feature/CE-609-infrastructure-remove-permissions-from-header 2023-08-17 11:46:43 -05:00
2e0d1dbb1c Updating to 0.19.0 2023-08-17 10:20:18 -05:00
a899db4b3e Merge tag 'version-0.18.0' into dev
Tag release
2023-08-17 10:20:14 -05:00
1a52e3354e Merge branch 'release/0.18.0' 2023-08-17 10:18:46 -05:00
c912fe7cc2 Update for next development version 2023-08-17 10:16:56 -05:00
0aa0f0a085 Update versions for release 2023-08-17 10:16:51 -05:00
4b9d7b135b Merge branch 'integration/sprint-31' into dev 2023-08-17 09:59:58 -05:00
7082f7c2b1 Merge pull request #38 from Kingsrook/feature/CE-567-script-writer-dev-setup-sdlc-ci-cd-setup
CE-567 Add concept of security lock Scope - e.g., READ-WRITE (blockin…
2023-08-15 19:41:31 -05:00
d28249e5ce Merge pull request #37 from Kingsrook/feature/CE-567-script-writer-dev-setup-sdlc-ci-cd-setup
CE-567 Add concept of security lock Scope - e.g., READ-WRITE (blockin…
2023-08-15 19:40:38 -05:00
7da34d70da CE-609 Remove tests for now-removed /api/oauth/token paths 2023-08-15 18:48:57 -05:00
0d0ab6c2e5 CE-567 Add concept of security lock Scope - e.g., READ-WRITE (blocking all access to a record), or just WRITE - which means anyone can read, but you must have the key to WRITE. 2023-08-15 16:55:36 -05:00
2577bbeb37 Restore QJavalinImplementation to original state after testHotSwap 2023-08-15 11:38:46 -05:00
db0b434e52 CE-609 - Support for staged rollout: Check sessionUUID before any other value; add logging. 2023-08-15 11:27:51 -05:00
d4e18d8f55 CE-608: updated check for jsonObject when processing API GET request to consider the object being jsonObject.isNull(), added ability to use CUSTOM authorization in an API util override 2023-08-14 19:37:00 -05:00
f2e674ded4 Merge pull request #36 from Kingsrook/feature/CE-607-mvp-of-transportation-plan-record
Feature/ce 607 mvp of transportation plan record
2023-08-09 12:27:46 -05:00
366639c882 CE-609 Increase javalin test coverage (manageSessions and hotSwap) 2023-08-09 10:31:59 -05:00
dbaad85ec7 CE-609 Restore usage of sessionId cookie/auth-key (used by a test on table-based auth) 2023-08-09 09:55:59 -05:00
8479ef4b90 Initial WIP Checkpoint of auth0 userSessions 2023-08-09 09:47:41 -05:00
1da85ce0a2 CE-607 Go go tests 2023-08-08 16:51:47 -05:00
5dfa10912e CE-607 Slight tweaks to exposed join field validation 2023-08-08 16:45:30 -05:00
05f2341099 CE-607 Instance validation for section-fields from join tables 2023-08-08 16:21:28 -05:00
3406929e75 process query joins in Get 2023-08-08 13:18:27 -05:00
c548952281 Fixing a case in query joins, where a joinMetaData was given, but it needed flipped. 2023-08-08 13:18:13 -05:00
d811ed725d Support api queryCriteria and orderBy for removed fields; more/better use of api names for tables & fields in openApi spec; pass qInstance through supplemental validation chain; 2023-08-08 13:17:11 -05:00
4cb00670ed CE-607 Switch a tryElse to a tryAndRequireNonNullElse, to avoid NPE 2023-08-04 19:39:28 -05:00
4cbd808a55 CE-607 add query joins to GetInput 2023-08-04 19:39:06 -05:00
fc17ef6106 Avoid an NPE if initial version not set 2023-08-04 16:50:56 -05:00
b01023e541 Turn down some noisy logs 2023-08-04 16:50:41 -05:00
a4df67f9f9 Attempt to fix building proper x.y.z versions by respecting tag version-x.y.z as one that shouldn't edit the pom 2023-08-04 16:49:55 -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
059dffb620 Merge pull request #23 from Kingsrook/dev
refresh querystats from dev
2023-06-22 19:07:15 -05:00
1822dd8189 add query stats to count, aggregate actions; add system/env prop checks; ready for initial dev deployment 2023-06-22 11:07:24 -05:00
b75fd29a57 Updating to 0.16.0 2023-06-22 10:41:50 -05:00
5ff91739d4 Merge tag 'version-0.15.0' into dev
Tag release
2023-06-22 10:41:46 -05:00
ada95431c1 Merge branch 'release/0.15.0' 2023-06-22 10:40:30 -05:00
c24b7cd84b Update for next development version 2023-06-22 10:38:42 -05:00
bf80eb1845 Update versions for release 2023-06-22 10:38:40 -05:00
db0c490a32 Merge pull request #20 from Kingsrook/feature/only-log-warnings-and-errors-once
initial version of attempting to downgrade logs if a warning or error…
2023-06-22 10:33:44 -05:00
a73bd40c6f dowgraded log to a debug 2023-06-22 10:32:56 -05:00
7eea0c08bb update to get rid of a few warnings 2023-06-22 10:19:08 -05:00
18c11d3869 Fix NPEs 2023-06-21 16:25:15 -05:00
a367ec717c Add of factory method 2023-06-21 16:18:13 -05:00
65e8f7a71f For runProcess, send the input object through processBodyToJsonString (e.g., for js to java object conversion0 2023-06-21 16:17:53 -05:00
900484c01c More consistent behavior of field labels & descriptions everywhere 2023-06-21 16:17:32 -05:00
6bfe0cd3ea Give error about null recordSecurityLock (vs null list of locks) 2023-06-21 16:17:01 -05:00
12d1de7135 Add labelMappings to instanceEnricher 2023-06-21 16:16:38 -05:00
e5efe8a64c Add message, current, and total to get-job-status 202(ACCEPTED) response spec 2023-06-20 11:43:30 -05:00
9d3cd50c7b Add 'tag' field to ApiProcessMetaData; use that when generating spec (for non-table processes for now) 2023-06-20 11:36:34 -05:00
5ae18c31f9 Merge pull request #22 from Kingsrook/feature/CTLE-509-support-sending-custom-data-fields-to-deposco
Feature/ctle 509 support sending custom data fields to deposco
2023-06-20 10:37:37 -05:00
ad11bf64db Merge branch 'integration/sprint-27' into feature/CTLE-509-support-sending-custom-data-fields-to-deposco 2023-06-20 10:37:20 -05:00
3a69957b43 Merge pull request #21 from Kingsrook/feature/CTLE-448-processes-via-api
Feature/ctle 448 processes via api
2023-06-20 10:36:35 -05:00
7af5ad2655 Fix to support null-filter id on table-triggers 2023-06-20 10:22:21 -05:00
57569e4c84 Escape identifiers in column names 2023-06-20 09:07:31 -05:00
3791c069c7 Add convertObjectToJava to code executors - for converting language objects to java objects 2023-06-20 09:07:31 -05:00
0f799339d6 Set user in new sessions 2023-06-19 12:33:01 -05:00
3e113a12b3 Initial checkin 2023-06-19 12:30:10 -05:00
79304adcb0 Move saved filter processes to qqq 2023-06-19 12:19:11 -05:00
2c192a3fd9 Add SavedFiltersMetaDataProvider (as we've introduced a dependency between it and ScriptsMetaDataProvider (through tableTriggers) 2023-06-19 12:14:26 -05:00
3772cf725f Make table-triggers respect saved filters 2023-06-19 12:03:50 -05:00
1cf83fb441 Add factory method: newForTable 2023-06-16 16:45:10 -05:00
4efb818bfe Add override executeWithStringDetails 2023-06-16 16:44:53 -05:00
f30b2a9ef8 Change times to 60 seconds 2023-06-16 16:44:23 -05:00
2efc732530 Fixing for CI 2023-06-16 09:55:06 -05:00
1ed51e0a35 Disabling these tests - poorly written, and no longer viable as a concept 2023-06-16 09:36:15 -05:00
54bf5bed8f Add some coverage for query stats; remove old classes (re-added in merge?) 2023-06-16 09:29:43 -05:00
12eb3be3cb Mark all fields as @QField 2023-06-16 08:44:17 -05:00
4105f034aa Merge remote-tracking branch 'origin/feature/query-stats' into feature/query-stats
# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java
#	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/data/QRecordEntity.java
2023-06-16 08:40:04 -05:00
a38d57c7af Update to work with new version of entities; actually working 2023-06-16 08:38:39 -05:00
14fa7fdb74 Update to only treat field as QField if @QField annotation is present 2023-06-16 08:38:03 -05:00
c07c007bc2 Add capability: QUERY_STATS; rework capabilities to be smarter w/ enable, then disable 2023-06-16 08:37:23 -05:00
9fe5067374 Initial checkin 2023-06-16 08:37:23 -05:00
599aff3487 Initial checkin 2023-06-16 08:36:04 -05:00
59b7e0529c Add getActionIdentity 2023-06-16 08:35:03 -05:00
d9a98c5987 Checkpoint - query stats (plus recordEntities with associations) 2023-06-15 10:19:31 -05:00
b58f93e627 Revert a few changes, to help with stability of generated api specs 2023-06-15 08:19:04 -05:00
1c1a0f99e8 changes to make script processes in api better 2023-06-14 16:42:04 -05:00
d273d091df Test fixes 2023-06-14 15:50:38 -05:00
d0194d9580 Missing copyright 2023-06-14 13:32:50 -05:00
54a0d6720f Missing copyright 2023-06-14 13:26:57 -05:00
19ee5bcb23 Checkpoint, WIP on processes in api - mostly done 2023-06-14 11:53:43 -05:00
eee7354e77 Checkpoint, WIP on processes in api 2023-06-12 18:19:48 -05:00
6b590324be initial version of attempting to downgrade logs if a warning or error has already been logged from the stack of throwables 2023-06-12 16:04:46 -05:00
a340299c67 Initial implementation of api processes 2023-06-12 10:58:30 -05:00
6a01754479 Renaming MiddlewareMetaData to SupplementalMetaData 2023-06-08 18:24:56 -05:00
4ccc726f2e Fix binding of long values 2023-06-08 14:38:27 -05:00
eb151f0610 Allow full JDBC URL to be set in RDBMS meta-data, used directly (w/ a known vendor) 2023-06-08 14:37:59 -05:00
a18d2afee5 Updating to 0.15.0 2023-06-08 14:27:29 -05:00
0007909792 Merge tag 'version-0.14.0' into dev
Tag release
2023-06-08 14:27:25 -05:00
78ba2b591c Update for next development version 2023-06-08 14:22:52 -05:00
0b525f8775 Checkpoint - query stats (plus recordEntities with associations) 2023-06-02 08:58:24 -05:00
279 changed files with 23314 additions and 2249 deletions

View File

@ -5,8 +5,8 @@ if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then
exit 1;
fi
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ]; then
echo "On a primary branch [$CIRCLE_BRANCH] - will not edit the pom version.";
if [ "$CIRCLE_BRANCH" == "dev" ] || [ "$CIRCLE_BRANCH" == "staging" ] || [ "$CIRCLE_BRANCH" == "main" ] || [ \! -z $(echo "$CIRCLE_TAG" | grep "^version-") ]; then
echo "On a primary branch or tag [${CIRCLE_BRANCH}${CIRCLE_TAG}] - will not edit the pom version.";
exit 0;
fi

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.14.0</revision>
<revision>0.19.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -50,6 +51,7 @@ public class AsyncJobManager
{
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
private String forcedJobUUID = null;
/*******************************************************************************
@ -69,7 +71,8 @@ public class AsyncJobManager
*******************************************************************************/
public <T extends Serializable> T startJob(String jobName, long timeout, TimeUnit timeUnit, AsyncJob<T> asyncJob) throws JobGoingAsyncException, QException
{
UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(UUID.randomUUID(), StateType.ASYNC_JOB_STATUS);
UUID jobUUID = StringUtils.hasContent(forcedJobUUID) ? UUID.fromString(forcedJobUUID) : UUID.randomUUID();
UUIDAndTypeStateKey uuidAndTypeStateKey = new UUIDAndTypeStateKey(jobUUID, StateType.ASYNC_JOB_STATUS);
AsyncJobStatus asyncJobStatus = new AsyncJobStatus();
asyncJobStatus.setState(AsyncJobState.RUNNING);
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
@ -205,4 +208,35 @@ public class AsyncJobManager
jobStatus.ifPresent(asyncJobStatus -> asyncJobStatus.setCancelRequested(true));
}
/*******************************************************************************
** Getter for forcedJobUUID
*******************************************************************************/
public String getForcedJobUUID()
{
return (this.forcedJobUUID);
}
/*******************************************************************************
** Setter for forcedJobUUID
*******************************************************************************/
public void setForcedJobUUID(String forcedJobUUID)
{
this.forcedJobUUID = forcedJobUUID;
}
/*******************************************************************************
** Fluent setter for forcedJobUUID
*******************************************************************************/
public AsyncJobManager withForcedJobUUID(String forcedJobUUID)
{
this.forcedJobUUID = forcedJobUUID;
return (this);
}
}

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

@ -29,6 +29,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
@ -44,10 +45,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -76,6 +79,21 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
** Execute to insert 1 audit, with a list of detail child records provided as just string messages
*******************************************************************************/
public static void executeWithStringDetails(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<String> detailMessages)
{
List<QRecord> detailRecords = null;
if(CollectionUtils.nullSafeHasContents(detailMessages))
{
detailRecords = detailMessages.stream().map(m -> new QRecord().withValue("message", m)).toList();
}
execute(tableName, recordId, securityKeyValues, message, detailRecords);
}
/*******************************************************************************
** Execute to insert 1 audit, with a list of detail child records
*******************************************************************************/
@ -92,12 +110,26 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
** Simple overload that internally figures out primary key and security key values
**
** Be aware - if the record doesn't have its security key values set (say it's a
** partial record as part of an update), then those values won't be in the
** security key map... This should probably be considered a bug.
*******************************************************************************/
public static void appendToInput(AuditInput auditInput, QTableMetaData table, QRecord record, String auditMessage)
{
appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.empty()), auditMessage);
}
/*******************************************************************************
** Add 1 auditSingleInput to an AuditInput object - with no details (child records).
*******************************************************************************/
public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
public static void appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
{
return (appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null));
appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null);
}
@ -123,6 +155,44 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
** For a given record, from a given table, build a map of the record's security
** key values.
**
** If, in case, the record has null value(s), and the oldRecord is given (e.g.,
** for the case of an update, where the record may not have all fields set, and
** oldRecord should be known for doing field-diffs), then try to get the value(s)
** from oldRecord.
**
** Currently, will leave values null if they aren't found after that.
**
** An alternative could be to re-fetch the record from its source if needed...
*******************************************************************************/
public static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record, Optional<QRecord> oldRecord)
{
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
Serializable keyValue = record == null ? null : record.getValue(recordSecurityLock.getFieldName());
if(keyValue == null && oldRecord.isPresent())
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()));
keyValue = oldRecord.get().getValue(recordSecurityLock.getFieldName());
}
if(keyValue == null)
{
LOG.debug("Table with a securityLock, but value not found in field", logPair("table", table.getName()), logPair("field", recordSecurityLock.getFieldName()), logPair("oldRecordIsPresent", oldRecord.isPresent()));
}
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), keyValue);
}
return securityKeyValues;
}
/*******************************************************************************
**
*******************************************************************************/
@ -151,7 +221,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
///////////////////////////////////////////////////
// validate security keys on the table are given //
///////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{

View File

@ -23,8 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.audits;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
@ -52,12 +54,12 @@ import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.actions.audits.AuditAction.getRecordSecurityKeyValues;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -69,6 +71,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
{
private static final QLogger LOG = QLogger.getLogger(DMLAuditAction.class);
public static final String AUDIT_CONTEXT_FIELD_NAME = "auditContext";
/*******************************************************************************
@ -98,34 +102,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
return (output);
}
String contextSuffix = "";
if(StringUtils.hasContent(input.getAuditContext()))
{
contextSuffix = " " + input.getAuditContext();
}
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
{
String processName = runProcessInput.getProcessName();
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process != null)
{
contextSuffix = " during process: " + process.getLabel();
}
}
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
String apiLabel = qSession.getValue("apiLabel");
if(!StringUtils.hasContent(apiLabel))
{
apiLabel = "API";
}
contextSuffix += (" via " + apiLabel + " Version: " + apiVersion);
}
String contextSuffix = getContentSuffix(input);
AuditInput auditInput = new AuditInput();
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
@ -136,7 +113,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord record : recordList)
{
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix);
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.empty()), "Record was " + dmlType.pastTenseVerb + contextSuffix);
}
}
else if(auditLevel.equals(AuditLevel.FIELD))
@ -146,7 +123,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
///////////////////////////////////////////////////////////////////
// do many audits, all with field level details, for FIELD level //
///////////////////////////////////////////////////////////////////
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), qSession);
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
//////////////////////////////////////////
@ -168,92 +145,8 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
List<QRecord> details = new ArrayList<>();
for(String fieldName : sortedFieldNames)
{
if(!record.getValues().containsKey(fieldName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if the stored record doesn't have this field name, then don't audit anything about it //
// this is to deal with our Patch style updates not looking like every field was cleared out. //
////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
{
continue;
}
QFieldMetaData field = table.getField(fieldName);
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
QRecord detailRecord = null;
if(oldRecord == null)
{
if(DMLType.INSERT.equals(dmlType) && value == null)
{
continue;
}
if(field.getType().equals(QFieldType.BLOB))
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
}
else
{
if(!Objects.equals(oldValue, value))
{
if(field.getType().equals(QFieldType.BLOB))
{
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
}
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
}
}
}
}
if(detailRecord != null)
{
detailRecord.withValue("fieldName", fieldName);
details.add(detailRecord);
}
makeAuditDetailRecordForField(fieldName, table, dmlType, record, oldRecord)
.ifPresent(details::add);
}
if(details.isEmpty() && DMLType.UPDATE.equals(dmlType))
@ -263,7 +156,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
}
else
{
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.ofNullable(oldRecord)), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
}
}
}
@ -283,6 +176,254 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
static String getContentSuffix(DMLAuditInput input)
{
StringBuilder contextSuffix = new StringBuilder();
/////////////////////////////////////////////////////////////////////////////
// start with context from the input wrapper //
// note, these contexts get propagated down from Input/Update/Delete Input //
/////////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(input.getAuditContext()))
{
contextSuffix.append(" ").append(input.getAuditContext());
}
/////////////////////////////////////////////////////////////////////////////////////
// note process label (and a possible context from the process's state) if present //
/////////////////////////////////////////////////////////////////////////////////////
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
{
String processAuditContext = ValueUtils.getValueAsString(runProcessInput.getValue(AUDIT_CONTEXT_FIELD_NAME));
if(StringUtils.hasContent(processAuditContext))
{
contextSuffix.append(" ").append(processAuditContext);
}
String processName = runProcessInput.getProcessName();
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process != null)
{
contextSuffix.append(" during process: ").append(process.getLabel());
}
}
///////////////////////////////////////////////////
// use api label & version if present in session //
///////////////////////////////////////////////////
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
String apiLabel = qSession.getValue("apiLabel");
if(!StringUtils.hasContent(apiLabel))
{
apiLabel = "API";
}
contextSuffix.append(" via ").append(apiLabel).append(" Version: ").append(apiVersion);
}
return (contextSuffix.toString());
}
/*******************************************************************************
**
*******************************************************************************/
static Optional<QRecord> makeAuditDetailRecordForField(String fieldName, QTableMetaData table, DMLType dmlType, QRecord record, QRecord oldRecord)
{
if(!record.getValues().containsKey(fieldName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if the stored record doesn't have this field name, then don't audit anything about it //
// this is to deal with our Patch style updates not looking like every field was cleared out. //
////////////////////////////////////////////////////////////////////////////////////////////////
return (Optional.empty());
}
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
{
return (Optional.empty());
}
QFieldMetaData field = table.getField(fieldName);
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
QRecord detailRecord = null;
if(oldRecord == null)
{
if(DMLType.INSERT.equals(dmlType) && value == null)
{
return (Optional.empty());
}
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
}
else
{
if(areValuesDifferentForAudit(field, value, oldValue))
{
if(field.getType().equals(QFieldType.BLOB) || field.getType().needsMasked())
{
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel());
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + field.getLabel());
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel());
}
}
else
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
}
}
}
}
if(detailRecord != null)
{
LOG.debug("Returning with message: " + detailRecord.getValueString("message"));
detailRecord.withValue("fieldName", fieldName);
return (Optional.of(detailRecord));
}
return (Optional.empty());
}
/*******************************************************************************
**
*******************************************************************************/
static boolean areValuesDifferentForAudit(QFieldMetaData field, Serializable value, Serializable oldValue)
{
try
{
///////////////////
// decimal rules //
///////////////////
if(field.getType().equals(QFieldType.DECIMAL))
{
BigDecimal newBD = ValueUtils.getValueAsBigDecimal(value);
BigDecimal oldBD = ValueUtils.getValueAsBigDecimal(oldValue);
if(newBD == null && oldBD == null)
{
return (false);
}
if(newBD == null || oldBD == null)
{
return (true);
}
return (newBD.compareTo(oldBD) != 0);
}
////////////////////
// dateTime rules //
////////////////////
if(field.getType().equals(QFieldType.DATE_TIME))
{
Instant newI = ValueUtils.getValueAsInstant(value);
Instant oldI = ValueUtils.getValueAsInstant(oldValue);
if(newI == null && oldI == null)
{
return (false);
}
if(newI == null || oldI == null)
{
return (true);
}
////////////////////////////////
// just compare to the second //
////////////////////////////////
return (newI.truncatedTo(ChronoUnit.SECONDS).compareTo(oldI.truncatedTo(ChronoUnit.SECONDS)) != 0);
}
//////////////////
// string rules //
//////////////////
if(field.getType().isStringLike())
{
String newString = ValueUtils.getValueAsString(value);
String oldString = ValueUtils.getValueAsString(oldValue);
boolean newIsNullOrEmpty = !StringUtils.hasContent(newString);
boolean oldIsNullOrEmpty = !StringUtils.hasContent(oldString);
if(newIsNullOrEmpty && oldIsNullOrEmpty)
{
return (false);
}
if(newIsNullOrEmpty || oldIsNullOrEmpty)
{
return (true);
}
return (newString.compareTo(oldString) != 0);
}
/////////////////////////////////////
// default just use Objects.equals //
/////////////////////////////////////
return !Objects.equals(oldValue, value);
}
catch(Exception e)
{
LOG.debug("Error checking areValuesDifferentForAudit", e, logPair("fieldName", field.getName()), logPair("value", value), logPair("oldValue", oldValue));
}
////////////////////////////////////
// default to something simple... //
////////////////////////////////////
return !Objects.equals(oldValue, value);
}
/*******************************************************************************
**
*******************************************************************************/
@ -372,21 +513,6 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
{
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
}
return securityKeyValues;
}
/*******************************************************************************
**
*******************************************************************************/
@ -406,7 +532,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
/*******************************************************************************
**
*******************************************************************************/
private enum DMLType
enum DMLType
{
INSERT("Inserted", true),
UPDATE("Edited", true),

View File

@ -22,9 +22,8 @@
package com.kingsrook.qqq.backend.core.actions.automation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -90,6 +89,10 @@ public class RecordAutomationStatusUpdater
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Avoid setting records to PENDING_INSERT or PENDING_UPDATE even if they don't have any insert or update automations or triggers //
// such records should go straight to OK status. //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(canWeSkipPendingAndGoToOkay(table, automationStatus))
{
automationStatus = AutomationStatus.OK;
@ -121,9 +124,13 @@ public class RecordAutomationStatusUpdater
** being asked to set status to PENDING_INSERT (or PENDING_UPDATE), then just
** move the status straight to OK.
*******************************************************************************/
private static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
static boolean canWeSkipPendingAndGoToOkay(QTableMetaData table, AutomationStatus automationStatus)
{
List<TableAutomationAction> tableActions = Objects.requireNonNullElse(table.getAutomationDetails().getActions(), new ArrayList<>());
List<TableAutomationAction> tableActions = Collections.emptyList();
if(table.getAutomationDetails() != null && table.getAutomationDetails().getActions() != null)
{
tableActions = table.getAutomationDetails().getActions();
}
if(automationStatus.equals(AutomationStatus.PENDING_INSERT_AUTOMATIONS))
{
@ -135,6 +142,12 @@ public class RecordAutomationStatusUpdater
{
return (false);
}
////////////////////////////////////////////////////////////////////////////////////////
// if we're going to pending-insert, and there are no insert automations or triggers, //
// then we may skip pending and go to okay. //
////////////////////////////////////////////////////////////////////////////////////////
return (true);
}
else if(automationStatus.equals(AutomationStatus.PENDING_UPDATE_AUTOMATIONS))
{
@ -146,9 +159,21 @@ public class RecordAutomationStatusUpdater
{
return (false);
}
}
return (true);
////////////////////////////////////////////////////////////////////////////////////////
// if we're going to pending-update, and there are no insert automations or triggers, //
// then we may skip pending and go to okay. //
////////////////////////////////////////////////////////////////////////////////////////
return (true);
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// if we're going to any other automation status - then we may never "skip pending" and go to okay - //
// because we weren't asked to go to pending! //
///////////////////////////////////////////////////////////////////////////////////////////////////////
return (false);
}
}

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

@ -39,12 +39,15 @@ import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
@ -61,11 +64,14 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.apache.commons.lang.NotImplementedException;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -270,16 +276,39 @@ public class PollingAutomationPerTableRunner implements Runnable
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
// todo - get filter if there is/was one
rs.add(new TableAutomationAction()
.withName("Script:" + record.getValue("scriptId"))
.withFilter(null)
.withTriggerEvent(triggerEvent)
.withPriority(record.getValueInteger("priority"))
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
.withValues(MapBuilder.of("scriptId", record.getValue("scriptId")))
.withIncludeRecordAssociations(true)
);
TableTrigger tableTrigger = new TableTrigger(record);
try
{
QQueryFilter filter = null;
Integer filterId = tableTrigger.getFilterId();
if(filterId != null)
{
GetInput getInput = new GetInput();
getInput.setTableName(SavedFilter.TABLE_NAME);
getInput.setPrimaryKey(filterId);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
SavedFilter savedFilter = new SavedFilter(getOutput.getRecord());
filter = JsonUtils.toObject(savedFilter.getFilterJson(), QQueryFilter.class);
}
}
rs.add(new TableAutomationAction()
.withName("Script:" + tableTrigger.getScriptId())
.withFilter(filter)
.withTriggerEvent(triggerEvent)
.withPriority(tableTrigger.getPriority())
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
.withValues(MapBuilder.of("scriptId", tableTrigger.getScriptId()))
.withIncludeRecordAssociations(true)
);
}
catch(Exception e)
{
LOG.error("Error setting up table trigger", e, logPair("tableTriggerId", tableTrigger.getId()));
}
}
}
@ -313,22 +342,9 @@ public class PollingAutomationPerTableRunner implements Runnable
boolean anyActionsFailed = false;
for(TableAutomationAction action : actions)
{
try
boolean hadError = applyActionToRecords(table, records, action);
if(hadError)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(table, matchingQRecords, action);
}
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
anyActionsFailed = true;
}
}
@ -348,6 +364,37 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Run one action over a list of records (if they match the action's filter).
**
** @return hadError - true if an exception was caught; false if all OK.
*******************************************************************************/
protected boolean applyActionToRecords(QTableMetaData table, List<QRecord> records, TableAutomationAction action)
{
try
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(table, matchingQRecords, action);
}
return (false);
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
return (true);
}
}
/*******************************************************************************
** For a given action, and a list of records - return a new list, of the ones
** which match the action's filter (if there is one - if not, then all match).

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

@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOu
** Interface for the Aggregate action.
**
*******************************************************************************/
public interface AggregateInterface
public interface AggregateInterface extends BaseQueryInterface
{
/*******************************************************************************
**

View File

@ -0,0 +1,81 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.interfaces;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
/*******************************************************************************
** Base class for "query" (e.g., read-operations) action interfaces (query, count, aggregate).
** Initially just here for the QueryStat methods - if we expand those to apply
** to insert/update/delete, well, then rename this maybe to BaseActionInterface?
*******************************************************************************/
public interface BaseQueryInterface
{
/*******************************************************************************
**
*******************************************************************************/
default void setQueryStat(QueryStat queryStat)
{
//////////
// noop //
//////////
}
/*******************************************************************************
**
*******************************************************************************/
default QueryStat getQueryStat()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
default void setQueryStatFirstResultTime()
{
QueryStat queryStat = getQueryStat();
if(queryStat != null)
{
if(queryStat.getFirstResultTimestamp() == null)
{
queryStat.setFirstResultTimestamp(Instant.now());
}
}
}
/*******************************************************************************
**
*******************************************************************************/
default void cancelAction()
{
//////////////////////////////////////////////
// initially at least, a noop in base class //
//////////////////////////////////////////////
}
}

View File

@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
** Interface for the Count action.
**
*******************************************************************************/
public interface CountInterface
public interface CountInterface extends BaseQueryInterface
{
/*******************************************************************************
**

View File

@ -31,10 +31,11 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
** Interface for the Query action.
**
*******************************************************************************/
public interface QueryInterface
public interface QueryInterface extends BaseQueryInterface
{
/*******************************************************************************
**
*******************************************************************************/
QueryOutput execute(QueryInput queryInput) throws QException;
}

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

@ -197,7 +197,27 @@ public class ExportAction
String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName))
{
queryJoins.add(new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true));
QueryJoin queryJoin = new QueryJoin(joinTableName).withType(QueryJoin.Type.LEFT).withSelect(true);
queryJoins.add(queryJoin);
/////////////////////////////////////////////////////////////////////////////////////////////
// in at least some cases, we need to let the queryJoin know what join-meta-data to use... //
// This code basically mirrors what QFMD is doing right now, so it's better - //
// but shouldn't all of this just be in JoinsContext? it does some of this... //
/////////////////////////////////////////////////////////////////////////////////////////////
QTableMetaData table = exportInput.getTable();
Optional<ExposedJoin> exposedJoinOptional = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> ej.getJoinTable().equals(joinTableName)).findFirst();
if(exposedJoinOptional.isEmpty())
{
throw (new QException("Could not find exposed join between base table " + table.getName() + " and requested join table " + joinTableName));
}
ExposedJoin exposedJoin = exposedJoinOptional.get();
if(exposedJoin.getJoinPath().size() == 1)
{
queryJoin.setJoinMetaData(QContext.getQInstance().getJoin(exposedJoin.getJoinPath().get(exposedJoin.getJoinPath().size() - 1)));
}
addedJoinNames.add(joinTableName);
}
}

View File

@ -23,22 +23,35 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.Log4jCodeExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QCodeException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.AbstractRunScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -101,6 +114,22 @@ public class ExecuteCodeAction
context.putAll(input.getInput());
}
//////////////////////////////////////////
// safely always set the deploymentMode //
//////////////////////////////////////////
context.put("deploymentMode", ObjectUtils.tryAndRequireNonNullElse(() -> QContext.getQInstance().getDeploymentMode(), null));
/////////////////////////////////////////////////////////////////////////////////
// set the qCodeExecutor into any context objects which are QCodeExecutorAware //
/////////////////////////////////////////////////////////////////////////////////
for(Serializable value : context.values())
{
if(value instanceof QCodeExecutorAware qCodeExecutorAware)
{
qCodeExecutorAware.setQCodeExecutor(qCodeExecutor);
}
}
Serializable codeOutput = qCodeExecutor.execute(codeReference, context, executionLogger);
output.setOutput(codeOutput);
executionLogger.acceptExecutionEnd(codeOutput);
@ -122,7 +151,17 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision)
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision) throws QException
{
return setupExecuteCodeInput(input, scriptRevision, null);
}
/*******************************************************************************
**
*******************************************************************************/
public static ExecuteCodeInput setupExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision, String fileName) throws QException
{
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
@ -139,7 +178,49 @@ public class ExecuteCodeAction
context.put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
if(CollectionUtils.nullSafeIsEmpty(scriptRevision.getFiles()))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(ScriptRevisionFile.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("scriptRevisionId", QCriteriaOperator.EQUALS, scriptRevision.getId())));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
scriptRevision.setFiles(new ArrayList<>());
for(QRecord record : queryOutput.getRecords())
{
scriptRevision.getFiles().add(new ScriptRevisionFile(record));
}
}
List<ScriptRevisionFile> files = scriptRevision.getFiles();
if(files == null || files.isEmpty())
{
throw (new QException("Script Revision " + scriptRevision.getId() + " had more than 1 associated ScriptRevisionFile (and the name to use was not specified)."));
}
else
{
String contents = null;
if(fileName == null || files.size() == 1)
{
contents = files.get(0).getContents();
}
else
{
for(ScriptRevisionFile file : files)
{
if(file.getFileName().equals(fileName))
{
contents = file.getContents();
}
}
if(contents == null)
{
throw (new QException("Could not find file named " + fileName + " for Script Revision " + scriptRevision.getId()));
}
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(contents).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
}
ExecuteCodeAction.addApiUtilityToContext(context, scriptRevision);
context.put("qqq", new QqqScriptUtils());
@ -157,7 +238,19 @@ public class ExecuteCodeAction
*******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, ScriptRevision scriptRevision)
{
if(!StringUtils.hasContent(scriptRevision.getApiName()) || !StringUtils.hasContent(scriptRevision.getApiVersion()))
addApiUtilityToContext(context, scriptRevision.getApiName(), scriptRevision.getApiVersion());
}
/*******************************************************************************
** Try to (dynamically) load the ApiScriptUtils object from the api middleware
** module -- in case the runtime doesn't have that module deployed (e.g, not in
** the project pom).
*******************************************************************************/
public static void addApiUtilityToContext(Map<String, Serializable> context, String apiName, String apiVersion)
{
if(!StringUtils.hasContent(apiName) || !StringUtils.hasContent(apiVersion))
{
return;
}
@ -165,7 +258,7 @@ public class ExecuteCodeAction
try
{
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(scriptRevision.getApiName(), scriptRevision.getApiVersion());
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(apiName, apiVersion);
context.put("api", (Serializable) apiScriptUtilsObject);
}
catch(ClassNotFoundException e)

View File

@ -41,4 +41,24 @@ public interface QCodeExecutor
*******************************************************************************/
Serializable execute(QCodeReference codeReference, Map<String, Serializable> inputContext, QCodeExecutionLoggerInterface executionLogger) throws QCodeException;
/*******************************************************************************
** Process an object from the script's language/runtime into a (more) native java object.
** e.g., a Nashorn ScriptObjectMirror will end up as a "primitive", or a List or Map of such
**
*******************************************************************************/
default Object convertObjectToJava(Object object) throws QCodeException
{
return (object);
}
/*******************************************************************************
** Convert a native java object into one for the script's language/runtime.
** e.g., a java Instant to a Nashorn Date
**
*******************************************************************************/
default Object convertJavaObject(Object object, Object requestedTypeHint) throws QCodeException
{
return (object);
}
}

View File

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

View File

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

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

@ -23,9 +23,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.AggregateInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -36,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class AggregateAction
{
private static final QLogger LOG = QLogger.getLogger(AggregateAction.class);
private AggregateInterface aggregateInterface;
/*******************************************************************************
**
*******************************************************************************/
@ -43,11 +55,36 @@ public class AggregateAction
{
ActionHelper.validateSession(aggregateInput);
QTableMetaData table = aggregateInput.getTable();
QBackendMetaData backend = aggregateInput.getBackend();
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, aggregateInput.getFilter());
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(aggregateInput.getBackend());
// todo pre-customization - just get to modify the request?
AggregateOutput aggregateOutput = qModule.getAggregateInterface().execute(aggregateInput);
// todo post-customization - can do whatever w/ the result if you want
aggregateInterface = qModule.getAggregateInterface();
aggregateInterface.setQueryStat(queryStat);
AggregateOutput aggregateOutput = aggregateInterface.execute(aggregateInput);
QueryStatManager.getInstance().add(queryStat);
return aggregateOutput;
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(aggregateInterface == null)
{
LOG.warn("aggregateInterface object was null when requested to cancel");
return;
}
aggregateInterface.cancelAction();
}
}

View File

@ -23,9 +23,15 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -36,6 +42,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class CountAction
{
private static final QLogger LOG = QLogger.getLogger(CountAction.class);
private CountInterface countInterface;
/*******************************************************************************
**
*******************************************************************************/
@ -43,11 +55,36 @@ public class CountAction
{
ActionHelper.validateSession(countInput);
QTableMetaData table = countInput.getTable();
QBackendMetaData backend = countInput.getBackend();
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, countInput.getFilter());
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
// todo pre-customization - just get to modify the request?
CountOutput countOutput = qModule.getCountInterface().execute(countInput);
// todo post-customization - can do whatever w/ the result if you want
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(countInput.getBackend());
countInterface = qModule.getCountInterface();
countInterface.setQueryStat(queryStat);
CountOutput countOutput = countInterface.execute(countInput);
QueryStatManager.getInstance().add(queryStat);
return countOutput;
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(countInterface == null)
{
LOG.warn("countInterface object was null when requested to cancel");
return;
}
countInterface.cancelAction();
}
}

View File

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

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

@ -34,8 +34,11 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryActionCacheHelper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.QueryStatManager;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -47,12 +50,14 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -71,6 +76,7 @@ public class QueryAction
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private QueryInput queryInput;
private QueryInterface queryInterface;
private QPossibleValueTranslator qPossibleValueTranslator;
@ -87,12 +93,14 @@ public class QueryAction
throw (new QException("Table name was not specified in query input"));
}
if(queryInput.getTable() == null)
QTableMetaData table = queryInput.getTable();
if(table == null)
{
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
}
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
QBackendMetaData backend = queryInput.getBackend();
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null)
@ -109,11 +117,24 @@ public class QueryAction
}
}
QueryStat queryStat = QueryStatManager.newQueryStat(backend, table, queryInput.getFilter());
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request?
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);
// todo post-customization - can do whatever w/ the result if you want
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(backend);
queryInterface = qModule.getQueryInterface();
queryInterface.setQueryStat(queryStat);
QueryOutput queryOutput = queryInterface.execute(queryInput);
QueryStatManager.getInstance().add(queryStat);
////////////////////////////
// handle cache use-cases //
////////////////////////////
if(table.getCacheOf() != null)
{
new QueryActionCacheHelper().handleCaching(queryInput, queryOutput);
}
if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe)
{
@ -319,4 +340,20 @@ public class QueryAction
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(queryInterface == null)
{
LOG.warn("queryInterface object was null when requested to cancel");
return;
}
queryInterface.cancelAction();
}
}

View File

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

View File

@ -61,6 +61,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
@ -83,6 +85,28 @@ public class UpdateAction
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(UpdateInput updateInput) throws QException
{
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
return (updateOutput.getRecords().get(0));
}
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecord> executeForRecords(UpdateInput updateInput) throws QException
{
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
return (updateOutput.getRecords());
}
/*******************************************************************************
**
*******************************************************************************/
@ -187,14 +211,16 @@ public class UpdateAction
{
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
}
else
{
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
}
if(updateInput.getInputSource().shouldValidateRequiredFields())
{
validateRequiredFields(updateInput);
}
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
@ -265,6 +291,8 @@ public class UpdateAction
QTableMetaData table = updateInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
{
List<Serializable> primaryKeysToLookup = new ArrayList<>();
@ -298,6 +326,8 @@ public class UpdateAction
}
}
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
for(QRecord record : page)
{
Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField()));
@ -310,6 +340,19 @@ public class UpdateAction
{
record.addError(new NotFoundStatusMessage("No record was found to update for " + primaryKeyField.getLabel() + " = " + value));
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////
// if the table has any write-only locks, validate their values here, on the old-records //
///////////////////////////////////////////////////////////////////////////////////////////
for(RecordSecurityLock lock : onlyWriteLocks)
{
QRecord oldRecord = lookedUpRecords.get(value);
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
}
}
}
}
}
@ -374,6 +417,11 @@ public class UpdateAction
//////////////////////////////////////////////////////
for(QRecord record : page)
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
continue;
}
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////

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

@ -0,0 +1,640 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatCriteriaField;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatJoinTable;
import com.kingsrook.qqq.backend.core.model.querystats.QueryStatOrderByField;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.tables.QQQTable;
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Singleton, which starts a thread, to store query stats into a table.
**
** Supports these systemProperties or ENV_VARS:
** qqq.queryStatManager.enabled / QQQ_QUERY_STAT_MANAGER_ENABLED
** qqq.queryStatManager.minMillisToStore / QQQ_QUERY_STAT_MANAGER_MIN_MILLIS_TO_STORE
** qqq.queryStatManager.jobPeriodSeconds / QQQ_QUERY_STAT_MANAGER_JOB_PERIOD_SECONDS
** qqq.queryStatManager.jobInitialDelay / QQQ_QUERY_STAT_MANAGER_JOB_INITIAL_DELAY
*******************************************************************************/
public class QueryStatManager
{
private static final QLogger LOG = QLogger.getLogger(QueryStatManager.class);
private static QueryStatManager queryStatManager = null;
// todo - support multiple qInstances?
private QInstance qInstance;
private Supplier<QSession> sessionSupplier;
private boolean active = false;
private List<QueryStat> queryStats = new ArrayList<>();
private ScheduledExecutorService executorService;
private int jobPeriodSeconds = 60;
private int jobInitialDelay = 60;
private int minMillisToStore = 0;
/*******************************************************************************
** Singleton constructor
*******************************************************************************/
private QueryStatManager()
{
}
/*******************************************************************************
** Singleton accessor
*******************************************************************************/
public static QueryStatManager getInstance()
{
if(queryStatManager == null)
{
queryStatManager = new QueryStatManager();
QMetaDataVariableInterpreter interpreter = new QMetaDataVariableInterpreter();
Integer propertyMinMillisToStore = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.minMillisToStore", "QQQ_QUERY_STAT_MANAGER_MIN_MILLIS_TO_STORE", null);
if(propertyMinMillisToStore != null)
{
queryStatManager.setMinMillisToStore(propertyMinMillisToStore);
}
Integer propertyJobPeriodSeconds = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.jobPeriodSeconds", "QQQ_QUERY_STAT_MANAGER_JOB_PERIOD_SECONDS", null);
if(propertyJobPeriodSeconds != null)
{
queryStatManager.setJobPeriodSeconds(propertyJobPeriodSeconds);
}
Integer propertyJobInitialDelay = interpreter.getIntegerFromPropertyOrEnvironment("qqq.queryStatManager.jobInitialDelay", "QQQ_QUERY_STAT_MANAGER_JOB_INITIAL_DELAY", null);
if(propertyJobInitialDelay != null)
{
queryStatManager.setJobInitialDelay(propertyJobInitialDelay);
}
}
return (queryStatManager);
}
/*******************************************************************************
**
*******************************************************************************/
public static QueryStat newQueryStat(QBackendMetaData backend, QTableMetaData table, QQueryFilter filter)
{
QueryStat queryStat = null;
if(table.isCapabilityEnabled(backend, Capability.QUERY_STATS))
{
queryStat = new QueryStat();
queryStat.setTableName(table.getName());
queryStat.setQueryFilter(Objects.requireNonNullElse(filter, new QQueryFilter()));
queryStat.setStartTimestamp(Instant.now());
}
return (queryStat);
}
/*******************************************************************************
**
*******************************************************************************/
public void start(QInstance qInstance, Supplier<QSession> sessionSupplier)
{
if(!isEnabled())
{
LOG.info("Not starting QueryStatManager per settings.");
return;
}
LOG.info("Starting QueryStatManager");
this.qInstance = qInstance;
this.sessionSupplier = sessionSupplier;
active = true;
queryStats = new ArrayList<>();
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), jobInitialDelay, jobPeriodSeconds, TimeUnit.SECONDS);
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean isEnabled()
{
return new QMetaDataVariableInterpreter().getBooleanFromPropertyOrEnvironment("qqq.queryStatManager.enabled", "QQQ_QUERY_STAT_MANAGER_ENABLED", true);
}
/*******************************************************************************
**
*******************************************************************************/
public void stop()
{
active = false;
queryStats.clear();
if(executorService != null)
{
executorService.shutdown();
executorService = null;
}
}
/*******************************************************************************
**
*******************************************************************************/
public void add(QueryStat queryStat)
{
if(queryStat == null)
{
return;
}
if(active)
{
////////////////////////////////////////////////////////////////////////////////////////
// set fields that we need to capture now (rather than when the thread to store runs) //
////////////////////////////////////////////////////////////////////////////////////////
if(queryStat.getFirstResultTimestamp() == null)
{
queryStat.setFirstResultTimestamp(Instant.now());
}
if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null)
{
long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli();
queryStat.setFirstResultMillis((int) millis);
}
if(queryStat.getFirstResultMillis() != null && queryStat.getFirstResultMillis() < minMillisToStore)
{
//////////////////////////////////////////////////////////////
// discard this record if it's under the min millis setting //
//////////////////////////////////////////////////////////////
return;
}
if(queryStat.getSessionId() == null && QContext.getQSession() != null)
{
queryStat.setSessionId(QContext.getQSession().getUuid());
}
if(queryStat.getAction() == null)
{
if(!QContext.getActionStack().isEmpty())
{
queryStat.setAction(QContext.getActionStack().peek().getActionIdentity());
}
else
{
boolean expected = false;
Exception e = new Exception("Unexpected empty action stack");
for(StackTraceElement stackTraceElement : e.getStackTrace())
{
String className = stackTraceElement.getClassName();
if(className.contains(QueryStatManagerInsertJob.class.getName()))
{
expected = true;
break;
}
}
if(!expected)
{
LOG.debug(e);
}
}
}
synchronized(this)
{
queryStats.add(queryStat);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private List<QueryStat> getListAndReset()
{
if(queryStats.isEmpty())
{
return Collections.emptyList();
}
synchronized(this)
{
List<QueryStat> returnList = queryStats;
queryStats = new ArrayList<>();
return (returnList);
}
}
/*******************************************************************************
** force stats to be stored right now (rather than letting the scheduled job do it)
*******************************************************************************/
public void storeStatsNow()
{
new QueryStatManagerInsertJob().run();
}
/*******************************************************************************
** Runnable that gets scheduled to periodically reset and store the list of collected stats
*******************************************************************************/
private static class QueryStatManagerInsertJob implements Runnable
{
private static final QLogger LOG = QLogger.getLogger(QueryStatManagerInsertJob.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void run()
{
try
{
QContext.init(getInstance().qInstance, getInstance().sessionSupplier.get());
/////////////////////////////////////////////////////////////////////////////////////
// every time we re-run, check if we've been turned off - if so, stop the service. //
/////////////////////////////////////////////////////////////////////////////////////
if(!isEnabled())
{
LOG.info("Stopping QueryStatManager.");
getInstance().stop();
return;
}
List<QueryStat> list = getInstance().getListAndReset();
LOG.info(logPair("queryStatListSize", list.size()));
if(list.isEmpty())
{
return;
}
////////////////////////////////////
// prime the entities for storing //
////////////////////////////////////
List<QRecord> queryStatQRecordsToInsert = new ArrayList<>();
for(QueryStat queryStat : list)
{
try
{
//////////////////////
// set the table id //
//////////////////////
Integer qqqTableId = getQQQTableId(queryStat.getTableName());
queryStat.setQqqTableId(qqqTableId);
//////////////////////////////
// build join-table records //
//////////////////////////////
if(CollectionUtils.nullSafeHasContents(queryStat.getJoinTableNames()))
{
List<QueryStatJoinTable> queryStatJoinTableList = new ArrayList<>();
for(String joinTableName : queryStat.getJoinTableNames())
{
queryStatJoinTableList.add(new QueryStatJoinTable().withQqqTableId(getQQQTableId(joinTableName)));
}
queryStat.setQueryStatJoinTableList(queryStatJoinTableList);
}
////////////////////////////
// build criteria records //
////////////////////////////
if(queryStat.getQueryFilter() != null && queryStat.getQueryFilter().hasAnyCriteria())
{
List<QueryStatCriteriaField> queryStatCriteriaFieldList = new ArrayList<>();
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, queryStat.getQueryFilter());
queryStat.setQueryStatCriteriaFieldList(queryStatCriteriaFieldList);
}
if(CollectionUtils.nullSafeHasContents(queryStat.getQueryFilter().getOrderBys()))
{
List<QueryStatOrderByField> queryStatOrderByFieldList = new ArrayList<>();
processOrderByFromFilter(qqqTableId, queryStatOrderByFieldList, queryStat.getQueryFilter());
queryStat.setQueryStatOrderByFieldList(queryStatOrderByFieldList);
}
queryStatQRecordsToInsert.add(queryStat.toQRecord());
}
catch(Exception e)
{
//////////////////////
// skip this record //
//////////////////////
LOG.warn("Error priming a query stat for storing", e);
}
}
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QueryStat.TABLE_NAME);
insertInput.setRecords(queryStatQRecordsToInsert);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.error("Error inserting query stats", e);
}
}
catch(Exception e)
{
LOG.warn("Error storing query stats", e);
}
finally
{
QContext.clear();
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processCriteriaFromFilter(Integer qqqTableId, List<QueryStatCriteriaField> queryStatCriteriaFieldList, QQueryFilter queryFilter) throws QException
{
for(QFilterCriteria criteria : CollectionUtils.nonNullList(queryFilter.getCriteria()))
{
String fieldName = criteria.getFieldName();
QueryStatCriteriaField queryStatCriteriaField = new QueryStatCriteriaField();
queryStatCriteriaField.setOperator(String.valueOf(criteria.getOperator()));
if(criteria.getValues() != null)
{
queryStatCriteriaField.setValues(StringUtils.join(",", criteria.getValues()));
}
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatCriteriaField.setQqqTableId(getQQQTableId(parts[0]));
queryStatCriteriaField.setName(parts[1]);
}
}
else
{
queryStatCriteriaField.setQqqTableId(qqqTableId);
queryStatCriteriaField.setName(fieldName);
}
queryStatCriteriaFieldList.add(queryStatCriteriaField);
}
for(QQueryFilter subFilter : CollectionUtils.nonNullList(queryFilter.getSubFilters()))
{
processCriteriaFromFilter(qqqTableId, queryStatCriteriaFieldList, subFilter);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void processOrderByFromFilter(Integer qqqTableId, List<QueryStatOrderByField> queryStatOrderByFieldList, QQueryFilter queryFilter) throws QException
{
for(QFilterOrderBy orderBy : CollectionUtils.nonNullList(queryFilter.getOrderBys()))
{
String fieldName = orderBy.getFieldName();
QueryStatOrderByField queryStatOrderByField = new QueryStatOrderByField();
if(fieldName != null)
{
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.");
if(parts.length > 1)
{
queryStatOrderByField.setQqqTableId(getQQQTableId(parts[0]));
queryStatOrderByField.setName(parts[1]);
}
}
else
{
queryStatOrderByField.setQqqTableId(qqqTableId);
queryStatOrderByField.setName(fieldName);
}
queryStatOrderByFieldList.add(queryStatOrderByField);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Integer getQQQTableId(String tableName) throws QException
{
/////////////////////////////
// look in the cache table //
/////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(QQQTablesMetaDataProvider.QQQ_TABLE_CACHE_TABLE_NAME);
getInput.setUniqueKey(MapBuilder.of("name", tableName));
GetOutput getOutput = new GetAction().execute(getInput);
////////////////////////
// upon cache miss... //
////////////////////////
if(getOutput.getRecord() == null)
{
///////////////////////////////////////////////////////
// insert the record (into the table, not the cache) //
///////////////////////////////////////////////////////
QTableMetaData tableMetaData = getInstance().qInstance.getTable(tableName);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(QQQTable.TABLE_NAME);
insertInput.setRecords(List.of(new QRecord().withValue("name", tableName).withValue("label", tableMetaData.getLabel())));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
///////////////////////////////////
// repeat the get from the cache //
///////////////////////////////////
getOutput = new GetAction().execute(getInput);
}
return getOutput.getRecord().getValueInteger("id");
}
}
/*******************************************************************************
** Getter for jobPeriodSeconds
*******************************************************************************/
public int getJobPeriodSeconds()
{
return (this.jobPeriodSeconds);
}
/*******************************************************************************
** Setter for jobPeriodSeconds
*******************************************************************************/
public void setJobPeriodSeconds(int jobPeriodSeconds)
{
this.jobPeriodSeconds = jobPeriodSeconds;
}
/*******************************************************************************
** Fluent setter for jobPeriodSeconds
*******************************************************************************/
public QueryStatManager withJobPeriodSeconds(int jobPeriodSeconds)
{
this.jobPeriodSeconds = jobPeriodSeconds;
return (this);
}
/*******************************************************************************
** Getter for jobInitialDelay
*******************************************************************************/
public int getJobInitialDelay()
{
return (this.jobInitialDelay);
}
/*******************************************************************************
** Setter for jobInitialDelay
*******************************************************************************/
public void setJobInitialDelay(int jobInitialDelay)
{
this.jobInitialDelay = jobInitialDelay;
}
/*******************************************************************************
** Fluent setter for jobInitialDelay
*******************************************************************************/
public QueryStatManager withJobInitialDelay(int jobInitialDelay)
{
this.jobInitialDelay = jobInitialDelay;
return (this);
}
/*******************************************************************************
** Getter for minMillisToStore
*******************************************************************************/
public int getMinMillisToStore()
{
return (this.minMillisToStore);
}
/*******************************************************************************
** Setter for minMillisToStore
*******************************************************************************/
public void setMinMillisToStore(int minMillisToStore)
{
this.minMillisToStore = minMillisToStore;
}
/*******************************************************************************
** Fluent setter for minMillisToStore
*******************************************************************************/
public QueryStatManager withMinMillisToStore(int minMillisToStore)
{
this.minMillisToStore = minMillisToStore;
return (this);
}
}

View File

@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -68,6 +69,7 @@ public class ValidateRecordSecurityLockHelper
{
INSERT,
UPDATE,
DELETE,
SELECT
}
@ -78,7 +80,7 @@ public class ValidateRecordSecurityLockHelper
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table);
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
{
return;
@ -98,11 +100,12 @@ public class ValidateRecordSecurityLockHelper
for(QRecord record : records)
{
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()))
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
/////////////////////////////////////////////////////////////////////////
// if not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// So if we're not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
@ -244,11 +247,18 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table)
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
{
List<RecordSecurityLock> recordSecurityLocks = table.getRecordSecurityLocks();
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
recordSecurityLocks = switch(action)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
};
////////////////////////////////////////
// if there are no locks, just return //
////////////////////////////////////////
@ -281,7 +291,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{
if(recordSecurityValue == null)
{

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

@ -136,7 +136,7 @@ public class QValueFormatter
{
return formatValue(displayFormat, ValueUtils.getValueAsBigDecimal(value));
}
else if(e.getMessage().equals("d != java.math.BigDecimal"))
else if(e.getMessage().equals("d != java.math.BigDecimal") || e.getMessage().equals("d != java.lang.String"))
{
return formatValue(displayFormat, ValueUtils.getValueAsInteger(value));
}

View File

@ -244,8 +244,6 @@ public class SearchPossibleValueSourceAction
}
}
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
// todo - skip & limit as params
queryFilter.setLimit(250);
@ -257,6 +255,9 @@ public class SearchPossibleValueSourceAction
input.getDefaultQueryFilter().addSubFilter(queryFilter);
queryFilter = input.getDefaultQueryFilter();
}
queryFilter.setOrderBys(possibleValueSource.getOrderByFields());
queryInput.setFilter(queryFilter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);

View File

@ -84,7 +84,7 @@ public class QContext
actionStackThreadLocal.get().add(actionInput);
}
if(!qInstance.getHasBeenValidated())
if(qInstance != null && !qInstance.getHasBeenValidated())
{
try
{

View File

@ -22,12 +22,19 @@
package com.kingsrook.qqq.backend.core.exceptions;
import org.apache.logging.log4j.Level;
/*******************************************************************************
* Base class for checked exceptions thrown in qqq.
*
*******************************************************************************/
public class QException extends Exception
{
private boolean hasLoggedWarning;
private boolean hasLoggedError;
/*******************************************************************************
** Constructor of message
@ -59,4 +66,102 @@ public class QException extends Exception
{
super(message, cause);
}
/*******************************************************************************
** Getter for hasLoggedWarning
*******************************************************************************/
public boolean getHasLoggedWarning()
{
return (this.hasLoggedWarning);
}
/*******************************************************************************
** Setter for hasLoggedWarning
*******************************************************************************/
public void setHasLoggedWarning(boolean hasLoggedWarning)
{
this.hasLoggedWarning = hasLoggedWarning;
}
/*******************************************************************************
** Fluent setter for hasLoggedWarning
*******************************************************************************/
public QException withHasLoggedWarning(boolean hasLoggedWarning)
{
this.hasLoggedWarning = hasLoggedWarning;
return (this);
}
/*******************************************************************************
** Getter for hasLoggedError
*******************************************************************************/
public boolean getHasLoggedError()
{
return (this.hasLoggedError);
}
/*******************************************************************************
** Setter for hasLoggedError
*******************************************************************************/
public void setHasLoggedError(boolean hasLoggedError)
{
this.hasLoggedError = hasLoggedError;
}
/*******************************************************************************
** Fluent setter for hasLoggedError
*******************************************************************************/
public QException withHasLoggedError(boolean hasLoggedError)
{
this.hasLoggedError = hasLoggedError;
return (this);
}
/*******************************************************************************
** helper function for getting if level logged
*******************************************************************************/
public boolean hasLoggedLevel(Level level)
{
if(Level.WARN.equals(level))
{
return (hasLoggedWarning);
}
if(Level.ERROR.equals(level))
{
return (hasLoggedError);
}
return (false);
}
/*******************************************************************************
** helper function for setting if level logged
*******************************************************************************/
public void setHasLoggedLevel(Level level)
{
if(Level.WARN.equals(level))
{
setHasLoggedWarning(true);
}
if(Level.ERROR.equals(level))
{
setHasLoggedError(true);
}
}
}

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@ -57,12 +58,13 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponen
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteLoadStep;
@ -96,6 +98,13 @@ public class QInstanceEnricher
//////////////////////////////////////////////////////////
private boolean configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels = true;
//////////////////////////////////////////////////////////////////////////////////////////////////
// let an instance define mappings to be applied during name-to-label enrichments, //
// e.g., to avoid ever incorrectly camel-casing an acronym (e.g., "Tla" should always be "TLA") //
// or to expand abbreviations in code (e.g., "Addr" should always be "Address" //
//////////////////////////////////////////////////////////////////////////////////////////////////
private static final Map<String, String> labelMappings = new LinkedHashMap<>();
/*******************************************************************************
@ -261,9 +270,9 @@ public class QInstanceEnricher
{
table.getFields().values().forEach(this::enrichField);
for(QMiddlewareTableMetaData middlewareTableMetaData : CollectionUtils.nonNullMap(table.getMiddlewareMetaData()).values())
for(QSupplementalTableMetaData supplementalTableMetaData : CollectionUtils.nonNullMap(table.getSupplementalMetaData()).values())
{
middlewareTableMetaData.enrich(table);
supplementalTableMetaData.enrich(qInstance, table);
}
}
@ -367,6 +376,11 @@ public class QInstanceEnricher
process.getStepList().forEach(this::enrichStep);
}
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
{
supplementalProcessMetaData.enrich(this, process);
}
enrichPermissionRules(process);
}
@ -641,7 +655,17 @@ public class QInstanceEnricher
////////////////////////////////////////////////////////////////
.replaceAll("([0-9])([A-Za-z])", "$1 $2");
return (name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix);
String label = name.substring(0, 1).toUpperCase(Locale.ROOT) + suffix;
/////////////////////////////////////////////////////////////////////////////////////////////
// apply any label mappings - e.g., to force app-specific acronyms/initialisms to all-caps //
/////////////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<String, String> entry : labelMappings.entrySet())
{
label = label.replaceAll(entry.getKey(), entry.getValue());
}
return (label);
}
@ -1105,4 +1129,35 @@ public class QInstanceEnricher
{
return (this.joinGraph);
}
/*******************************************************************************
**
*******************************************************************************/
public static void addLabelMapping(String from, String to)
{
labelMappings.put(from, to);
}
/*******************************************************************************
**
*******************************************************************************/
public static void removeLabelMapping(String from)
{
labelMappings.remove(from);
}
/*******************************************************************************
**
*******************************************************************************/
public static void clearLabelMappings()
{
labelMappings.clear();
}
}

View File

@ -48,7 +48,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
@ -63,6 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QSupplementalProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField;
@ -158,7 +159,7 @@ public class QInstanceValidator
validateQueuesAndProviders(qInstance);
validateJoins(qInstance);
validateSecurityKeyTypes(qInstance);
validateMiddlewareMetaData(qInstance);
validateSupplementalMetaData(qInstance);
validateUniqueTopLevelNames(qInstance);
}
@ -182,11 +183,11 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateMiddlewareMetaData(QInstance qInstance)
private void validateSupplementalMetaData(QInstance qInstance)
{
for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values())
for(QSupplementalInstanceMetaData supplementalInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getSupplementalMetaData()).values())
{
middlewareInstanceMetaData.validate(qInstance, this);
supplementalInstanceMetaData.validate(qInstance, this);
}
}
@ -437,10 +438,13 @@ public class QInstanceValidator
for(QFieldSection section : table.getSections())
{
validateTableSection(qInstance, table, section, fieldNamesInSections);
if(section.getTier().equals(Tier.T1))
if(assertCondition(section.getTier() != null, "Table " + tableName + " " + section.getName() + " is missing its tier"))
{
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
tier1Section = section;
if(section.getTier().equals(Tier.T1))
{
assertCondition(tier1Section == null, "Table " + tableName + " has more than 1 section listed as Tier 1");
tier1Section = section;
}
}
assertCondition(!usedSectionNames.contains(section.getName()), "Table " + tableName + " has more than 1 section named " + section.getName());
@ -572,6 +576,11 @@ public class QInstanceValidator
RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
{
continue;
}
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
{
@ -580,6 +589,8 @@ public class QInstanceValidator
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{
@ -1093,13 +1104,39 @@ public class QInstanceValidator
boolean hasFields = CollectionUtils.nullSafeHasContents(section.getFieldNames());
boolean hasWidget = StringUtils.hasContent(section.getWidgetName());
if(assertCondition(hasFields || hasWidget, "Table " + table.getName() + " section " + section.getName() + " does not have any fields or a widget."))
String sectionPrefix = "Table " + table.getName() + " section " + section.getName() + " ";
if(assertCondition(hasFields || hasWidget, sectionPrefix + "does not have any fields or a widget."))
{
if(table.getFields() != null && hasFields)
{
for(String fieldName : section.getFieldNames())
{
assertCondition(table.getFields().containsKey(fieldName), "Table " + table.getName() + " section " + section.getName() + " specifies fieldName " + fieldName + ", which is not a field on this table.");
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - this was originally written as an assertion: //
// if(assertCondition(qInstance.getTable(otherTableName) != null, sectionPrefix + "join-field " + fieldName + ", which is referencing an unrecognized table name [" + otherTableName + "]")) //
// but... then a field name with dots gives us a bad time here, so... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(fieldName.contains(".") && qInstance.getTable(fieldName.split("\\.")[0]) != null)
{
String[] parts = fieldName.split("\\.");
String otherTableName = parts[0];
String foreignFieldName = parts[1];
if(assertCondition(qInstance.getTable(otherTableName) != null, sectionPrefix + "join-field " + fieldName + ", which is referencing an unrecognized table name [" + otherTableName + "]"))
{
List<ExposedJoin> matchedExposedJoins = CollectionUtils.nonNullList(table.getExposedJoins()).stream().filter(ej -> otherTableName.equals(ej.getJoinTable())).toList();
if(assertCondition(CollectionUtils.nullSafeHasContents(matchedExposedJoins), sectionPrefix + "join-field " + fieldName + ", referencing table [" + otherTableName + "] which is not an exposed join on this table."))
{
assertCondition(!matchedExposedJoins.get(0).getIsMany(qInstance), sectionPrefix + "join-field " + fieldName + " references an is-many join, which is not supported.");
}
assertCondition(qInstance.getTable(otherTableName).getFields().containsKey(foreignFieldName), sectionPrefix + "join-field " + fieldName + " specifies a fieldName [" + foreignFieldName + "] which does not exist in that table [" + otherTableName + "].");
}
}
else
{
assertCondition(table.getFields().containsKey(fieldName), sectionPrefix + "specifies fieldName " + fieldName + ", which is not a field on this table.");
}
assertCondition(!fieldNamesInSections.contains(fieldName), "Table " + table.getName() + " has field " + fieldName + " listed more than once in its field sections.");
fieldNamesInSections.add(fieldName);
@ -1107,7 +1144,7 @@ public class QInstanceValidator
}
else if(hasWidget)
{
assertCondition(qInstance.getWidget(section.getWidgetName()) != null, "Table " + table.getName() + " section " + section.getName() + " specifies widget " + section.getWidgetName() + ", which is not a widget in this instance.");
assertCondition(qInstance.getWidget(section.getWidgetName()) != null, sectionPrefix + "specifies widget " + section.getWidgetName() + ", which is not a widget in this instance.");
}
}
}
@ -1218,14 +1255,18 @@ public class QInstanceValidator
QScheduleMetaData schedule = process.getSchedule();
assertCondition(schedule.getRepeatMillis() != null || schedule.getRepeatSeconds() != null, "Either repeat millis or repeat seconds must be set on schedule in process " + processName);
if(schedule.getBackendVariant() != null)
if(schedule.getVariantBackend() != null)
{
assertCondition(schedule.getVariantRunStrategy() != null, "A variant strategy was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(schedule.getVariantTableName() != null, "A variant table name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(schedule.getVariantFieldName() != null, "A variant field name was not set for " + schedule.getBackendVariant() + " on schedule in process " + processName);
assertCondition(qInstance.getBackend(schedule.getVariantBackend()) != null, "A variant backend was not found for " + schedule.getVariantBackend());
assertCondition(schedule.getVariantRunStrategy() != null, "A variant run strategy was not set for " + schedule.getVariantBackend() + " on schedule in process " + processName);
}
}
for(QSupplementalProcessMetaData supplementalProcessMetaData : CollectionUtils.nonNullMap(process.getSupplementalMetaData()).values())
{
supplementalProcessMetaData.validate(qInstance, process, this);
}
});
}
}
@ -1703,7 +1744,7 @@ public class QInstanceValidator
** But if it throws, add the provided message to the list of errors (and return false,
** e.g., in case you need to stop evaluating rules to avoid exceptions).
*******************************************************************************/
private boolean assertNoException(UnsafeLambda unsafeLambda, String message)
public boolean assertNoException(UnsafeLambda unsafeLambda, String message)
{
try
{
@ -1736,7 +1777,7 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void warn(String message)
public void warn(String message)
{
if(printWarnings)
{

View File

@ -30,6 +30,7 @@ import java.util.Locale;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
@ -266,4 +267,111 @@ public class QMetaDataVariableInterpreter
valueMaps.put(name, values);
}
/*******************************************************************************
** First look for a boolean ("true" or "false") in the specified system property -
** Next look for a boolean in the specified env var name -
** Finally return the default.
*******************************************************************************/
public boolean getBooleanFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, boolean defaultIfNotSet)
{
String propertyValue = System.getProperty(systemPropertyName);
if(StringUtils.hasContent(propertyValue))
{
if("false".equalsIgnoreCase(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as boolean false.");
return (false);
}
else if("true".equalsIgnoreCase(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as boolean true.");
return (true);
}
else
{
LOG.warn("Unrecognized boolean value [" + propertyValue + "] for system property [" + systemPropertyName + "].");
}
}
String envValue = interpret("${env." + environmentVariableName + "}");
if(StringUtils.hasContent(envValue))
{
if("false".equalsIgnoreCase(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as boolean false.");
return (false);
}
else if("true".equalsIgnoreCase(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as boolean true.");
return (true);
}
else
{
LOG.warn("Unrecognized boolean value [" + envValue + "] for env var [" + environmentVariableName + "].");
}
}
return defaultIfNotSet;
}
/*******************************************************************************
** First look for an Integer in the specified system property -
** Next look for an Integer in the specified env var name -
** Finally return the default (null allowed as default!)
*******************************************************************************/
public Integer getIntegerFromPropertyOrEnvironment(String systemPropertyName, String environmentVariableName, Integer defaultIfNotSet)
{
String propertyValue = System.getProperty(systemPropertyName);
if(StringUtils.hasContent(propertyValue))
{
if(canParseAsInteger(propertyValue))
{
LOG.info("Read system property [" + systemPropertyName + "] as integer " + propertyValue);
return (Integer.parseInt(propertyValue));
}
else
{
LOG.warn("Unrecognized integer value [" + propertyValue + "] for system property [" + systemPropertyName + "].");
}
}
String envValue = interpret("${env." + environmentVariableName + "}");
if(StringUtils.hasContent(envValue))
{
if(canParseAsInteger(envValue))
{
LOG.info("Read env var [" + environmentVariableName + "] as integer " + environmentVariableName);
return (Integer.parseInt(envValue));
}
else
{
LOG.warn("Unrecognized integer value [" + envValue + "] for env var [" + environmentVariableName + "].");
}
}
return defaultIfNotSet;
}
/*******************************************************************************
** we'd use NumberUtils.isDigits, but that doesn't allow negatives, or
** numberUtils.isParseable, but that allows decimals, so...
*******************************************************************************/
private boolean canParseAsInteger(String value)
{
if(value == null)
{
return (false);
}
return (value.matches("^-?[0-9]+$"));
}
}

View File

@ -29,10 +29,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -392,7 +394,7 @@ public class QLogger
*******************************************************************************/
public void warn(String message, Throwable t)
{
logger.warn(makeJsonString(message, t));
logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t));
}
@ -402,7 +404,7 @@ public class QLogger
*******************************************************************************/
public void warn(String message, Throwable t, LogPair... logPairs)
{
logger.warn(makeJsonString(message, t, logPairs));
logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(message, t, logPairs));
}
@ -412,7 +414,7 @@ public class QLogger
*******************************************************************************/
public void warn(Throwable t)
{
logger.warn(makeJsonString(null, t));
logger.log(determineIfShouldDowngrade(t, Level.WARN), makeJsonString(null, t));
}
@ -452,7 +454,7 @@ public class QLogger
*******************************************************************************/
public void error(String message, Throwable t)
{
logger.error(makeJsonString(message, t));
logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t));
}
@ -462,7 +464,7 @@ public class QLogger
*******************************************************************************/
public void error(String message, Throwable t, LogPair... logPairs)
{
logger.error(makeJsonString(message, t, logPairs));
logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(message, t, logPairs));
}
@ -472,7 +474,7 @@ public class QLogger
*******************************************************************************/
public void error(Throwable t)
{
logger.error(makeJsonString(null, t));
logger.log(determineIfShouldDowngrade(t, Level.ERROR), makeJsonString(null, t));
}
@ -532,7 +534,7 @@ public class QLogger
if(t != null)
{
logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t))));
logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(t))));
}
return (LogUtils.jsonLog(logPairList));
@ -582,4 +584,40 @@ public class QLogger
}
}
}
/*******************************************************************************
**
*******************************************************************************/
protected Level determineIfShouldDowngrade(Throwable t, Level level)
{
//////////////////////////////////////////////////////////////////////////////////////
// look for QExceptions in the chain, if none found, return the log level passed in //
//////////////////////////////////////////////////////////////////////////////////////
List<QException> exceptionList = ExceptionUtils.getClassListFromRootChain(t, QException.class);
if(CollectionUtils.nullSafeIsEmpty(exceptionList))
{
return (level);
}
////////////////////////////////////////////////////////////////////
// check if any QException in this chain to see if it has already //
// logged this level, if so, downgrade to INFO //
////////////////////////////////////////////////////////////////////
for(QException qException : exceptionList)
{
if(qException.hasLoggedLevel(level))
{
log(Level.DEBUG, "Downgrading log message from " + level.toString() + " to " + Level.INFO, t);
return (Level.INFO);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// if it has not logged at this level, set that it has in QException, and return passed in level //
///////////////////////////////////////////////////////////////////////////////////////////////////
exceptionList.get(0).setHasLoggedLevel(level);
return (level);
}
}

View File

@ -56,6 +56,16 @@ public class AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public String getActionIdentity()
{
return (getClass().getSimpleName());
}
/*******************************************************************************
** performance instance validation (if not previously done).
* // todo - verify this is happening (e.g., when context is set i guess)
@ -145,14 +155,4 @@ public class AbstractActionInput
this.asyncJobCallback = asyncJobCallback;
}
/*******************************************************************************
** Fluent setter for instance
*******************************************************************************/
public AbstractActionInput withInstance(QInstance instance)
{
return (this);
}
}

View File

@ -46,6 +46,17 @@ public class AbstractTableActionInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getActionIdentity()
{
return (getClass().getSimpleName() + ":" + getTableName());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -246,7 +247,7 @@ public class AuditSingleInput
setAuditTableName(table.getName());
this.securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName()));
}

View File

@ -22,8 +22,10 @@
package com.kingsrook.qqq.backend.core.model.actions.processes;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -76,6 +78,35 @@ public class ProcessSummaryFilterLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public String getFullText()
{
StringBuilder rs = new StringBuilder();
if(StringUtils.hasContent(linkPreText))
{
rs.append(linkPreText).append(" ");
}
if(StringUtils.hasContent(linkText))
{
rs.append(linkText).append(" ");
}
if(StringUtils.hasContent(linkPostText))
{
rs.append(linkPostText).append(" ");
}
rs.deleteCharAt(rs.length() - 1);
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -395,15 +396,19 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
{
if(count != null)
{
String baseMessage;
if(count.equals(1))
{
setMessage((isPast ? getSingularPastMessage() : getSingularFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
baseMessage = isPast ? getSingularPastMessage() : getSingularFutureMessage();
}
else
{
setMessage((isPast ? getPluralPastMessage() : getPluralFutureMessage())
+ (messageSuffix == null ? "" : messageSuffix));
baseMessage = isPast ? getPluralPastMessage() : getPluralFutureMessage();
}
if(StringUtils.hasContent(baseMessage))
{
setMessage(baseMessage + ObjectUtils.requireConditionElse(messageSuffix, StringUtils::hasContent, ""));
}
}
}

View File

@ -23,7 +23,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -64,6 +66,35 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public String getFullText()
{
StringBuilder rs = new StringBuilder();
if(StringUtils.hasContent(linkPreText))
{
rs.append(linkPreText).append(" ");
}
if(StringUtils.hasContent(linkText))
{
rs.append(linkText).append(" ");
}
if(StringUtils.hasContent(linkPostText))
{
rs.append(linkPostText).append(" ");
}
rs.deleteCharAt(rs.length() - 1);
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,10 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
@ -31,6 +35,7 @@ import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -71,6 +76,17 @@ public class RunProcessInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getActionIdentity()
{
return (getClass().getSimpleName() + ":" + getProcessName());
}
/*******************************************************************************
** e.g., for steps after the first step in a process, seed the data in a run
** function request from a process state.
@ -183,6 +199,17 @@ public class RunProcessInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public RunProcessInput withValue(String fieldName, Serializable value)
{
this.processState.getValues().put(fieldName, value);
return (this);
}
/*******************************************************************************
** Setter for values
**
@ -258,7 +285,7 @@ public class RunProcessInput extends AbstractActionInput
*******************************************************************************/
public String getValueString(String fieldName)
{
return ((String) getValue(fieldName));
return (ValueUtils.getValueAsString(getValue(fieldName)));
}
@ -269,7 +296,67 @@ public class RunProcessInput extends AbstractActionInput
*******************************************************************************/
public Integer getValueInteger(String fieldName)
{
return ((Integer) getValue(fieldName));
return (ValueUtils.getValueAsInteger(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getValueBigDecimal(String fieldName)
{
return (ValueUtils.getValueAsBigDecimal(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public Boolean getValueBoolean(String fieldName)
{
return (ValueUtils.getValueAsBoolean(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public LocalTime getValueLocalTime(String fieldName)
{
return (ValueUtils.getValueAsLocalTime(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public LocalDate getValueLocalDate(String fieldName)
{
return (ValueUtils.getValueAsLocalDate(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public byte[] getValueByteArray(String fieldName)
{
return (ValueUtils.getValueAsByteArray(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public Instant getValueInstant(String fieldName)
{
return (ValueUtils.getValueAsInstant(getValue(fieldName)));
}

View File

@ -23,11 +23,16 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -122,6 +127,99 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
/*******************************************************************************
** Getter for a single field's value
**
*******************************************************************************/
public Serializable getValue(String fieldName)
{
return (this.processState.getValues().get(fieldName));
}
/*******************************************************************************
** Getter for a single field's value
**
*******************************************************************************/
public String getValueString(String fieldName)
{
return (ValueUtils.getValueAsString(getValue(fieldName)));
}
/*******************************************************************************
** Getter for a single field's value
**
*******************************************************************************/
public Integer getValueInteger(String fieldName)
{
return (ValueUtils.getValueAsInteger(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public BigDecimal getValueBigDecimal(String fieldName)
{
return (ValueUtils.getValueAsBigDecimal(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public Boolean getValueBoolean(String fieldName)
{
return (ValueUtils.getValueAsBoolean(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public LocalTime getValueLocalTime(String fieldName)
{
return (ValueUtils.getValueAsLocalTime(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public LocalDate getValueLocalDate(String fieldName)
{
return (ValueUtils.getValueAsLocalDate(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public byte[] getValueByteArray(String fieldName)
{
return (ValueUtils.getValueAsByteArray(getValue(fieldName)));
}
/*******************************************************************************
**
*******************************************************************************/
public Instant getValueInstant(String fieldName)
{
return (ValueUtils.getValueAsInstant(getValue(fieldName)));
}
/*******************************************************************************
** Setter for values
**
@ -133,6 +231,17 @@ public class RunProcessOutput extends AbstractActionOutput implements Serializab
/*******************************************************************************
**
*******************************************************************************/
public RunProcessOutput withValue(String fieldName, Serializable value)
{
this.processState.getValues().put(fieldName, value);
return (this);
}
/*******************************************************************************
** Setter for values
**

View File

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

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

@ -56,4 +56,14 @@ public enum AggregateOperator
{
return sqlPrefix;
}
/*******************************************************************************
**
*******************************************************************************/
public Aggregate of(String fieldName)
{
return (new Aggregate(fieldName, 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

@ -23,17 +23,21 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.QueryOrGetInputInterface;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
/*******************************************************************************
** Input data for the Get action
**
*******************************************************************************/
public class GetInput extends AbstractTableActionInput
public class GetInput extends AbstractTableActionInput implements QueryOrGetInputInterface
{
private QBackendTransaction transaction;
@ -46,6 +50,7 @@ public class GetInput extends AbstractTableActionInput
private boolean shouldOmitHiddenFields = true;
private boolean shouldMaskPasswords = true;
private List<QueryJoin> queryJoins = null;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
@ -66,6 +71,29 @@ public class GetInput extends AbstractTableActionInput
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public GetInput(String tableName)
{
setTableName(tableName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AbstractTableActionInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
** Getter for primaryKey
**
@ -387,4 +415,51 @@ public class GetInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for queryJoins
*******************************************************************************/
public List<QueryJoin> getQueryJoins()
{
return (this.queryJoins);
}
/*******************************************************************************
** Setter for queryJoins
*******************************************************************************/
public void setQueryJoins(List<QueryJoin> queryJoins)
{
this.queryJoins = queryJoins;
}
/*******************************************************************************
** Fluent setter for queryJoins
*******************************************************************************/
public GetInput withQueryJoins(List<QueryJoin> queryJoins)
{
this.queryJoins = queryJoins;
return (this);
}
/*******************************************************************************
** Fluent setter for queryJoins
**
*******************************************************************************/
public GetInput withQueryJoin(QueryJoin queryJoin)
{
if(this.queryJoins == null)
{
this.queryJoins = new ArrayList<>();
}
this.queryJoins.add(queryJoin);
return (this);
}
}

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

@ -30,17 +30,21 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
@ -60,6 +64,7 @@ public class JoinsContext
// note - will have entries for all tables, not just aliases. //
////////////////////////////////////////////////////////////////
private final Map<String, String> aliasToTableNameMap = new HashMap<>();
private Level logLevel = Level.OFF;
@ -69,62 +74,23 @@ public class JoinsContext
*******************************************************************************/
public JoinsContext(QInstance instance, String tableName, List<QueryJoin> queryJoins, QQueryFilter filter) throws QException
{
log("--- START ----------------------------------------------------------------------", logPair("mainTable", tableName));
this.instance = instance;
this.mainTableName = tableName;
this.queryJoins = new MutableList<>(queryJoins);
for(QueryJoin queryJoin : this.queryJoins)
{
log("Processing input query join", logPair("joinTable", queryJoin.getJoinTable()), logPair("alias", queryJoin.getAlias()), logPair("baseTableOrAlias", queryJoin.getBaseTableOrAlias()), logPair("joinMetaDataName", () -> queryJoin.getJoinMetaData().getName()));
processQueryJoin(queryJoin);
}
///////////////////////////////////////////////////////////////
// ensure any joins that contribute a recordLock are present //
///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = instance.getTable(mainTableName);
for(String joinName : joinNameChain)
{
if(this.queryJoins.stream().anyMatch(queryJoin ->
{
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}))
{
continue;
}
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin);
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin); //
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
}
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
}
ensureFilterIsRepresented(filter);
@ -141,6 +107,86 @@ public class JoinsContext
}
}
*/
log("Constructed JoinsContext", logPair("mainTableName", this.mainTableName), logPair("queryJoins", this.queryJoins.stream().map(qj -> qj.getJoinTable()).collect(Collectors.joining(","))));
log("--- END ------------------------------------------------------------------------");
}
/*******************************************************************************
**
*******************************************************************************/
private void ensureRecordSecurityLockIsRepresented(QInstance instance, String tableName, RecordSecurityLock recordSecurityLock) throws QException
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
log("Evaluating recordSecurityLock", logPair("recordSecurityLock", recordSecurityLock.getFieldName()), logPair("joinNameChain", joinNameChain));
QTableMetaData tmpTable = instance.getTable(mainTableName);
for(String joinName : joinNameChain)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////
// check the joins currently in the query - if any are for this table, then we don't need to add one //
///////////////////////////////////////////////////////////////////////////////////////////////////////
List<QueryJoin> matchingJoins = this.queryJoins.stream().filter(queryJoin ->
{
QJoinMetaData joinMetaData = null;
if(queryJoin.getJoinMetaData() != null)
{
joinMetaData = queryJoin.getJoinMetaData();
}
else
{
joinMetaData = findJoinMetaData(instance, tableName, queryJoin.getJoinTable());
}
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}).toList();
if(CollectionUtils.nullSafeHasContents(matchingJoins))
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - if a user added a join as an outer type, we need to change it to be inner, for the security purpose. //
// todo - is this always right? what about nulls-allowed? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
log("- skipping join already in the query", logPair("joinName", joinName));
if(matchingJoins.get(0).getType().equals(QueryJoin.Type.LEFT) || matchingJoins.get(0).getType().equals(QueryJoin.Type.RIGHT))
{
log("- - although... it was here as an outer - so switching it to INNER", logPair("joinName", joinName));
matchingJoins.get(0).setType(QueryJoin.Type.INNER);
}
continue;
}
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (non-flipped)");
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin, "forRecordSecurityLock (flipped)");
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
}
}
@ -151,8 +197,15 @@ public class JoinsContext
** use this method to add to the list, instead of ever adding directly, as it's
** important do to that process step (and we've had bugs when it wasn't done).
*******************************************************************************/
private void addQueryJoin(QueryJoin queryJoin) throws QException
private void addQueryJoin(QueryJoin queryJoin, String reason) throws QException
{
log("Adding query join to context",
logPair("reason", reason),
logPair("joinTable", queryJoin.getJoinTable()),
logPair("joinMetaData.name", () -> queryJoin.getJoinMetaData().getName()),
logPair("joinMetaData.leftTable", () -> queryJoin.getJoinMetaData().getLeftTable()),
logPair("joinMetaData.rightTable", () -> queryJoin.getJoinMetaData().getRightTable())
);
this.queryJoins.add(queryJoin);
processQueryJoin(queryJoin);
}
@ -177,10 +230,46 @@ public class JoinsContext
addedJoin = false;
for(QueryJoin queryJoin : queryJoins)
{
/////////////////////////////////////////////////////////////////////
// if the join has joinMetaData, then we don't need to process it. //
/////////////////////////////////////////////////////////////////////
if(queryJoin.getJoinMetaData() == null)
///////////////////////////////////////////////////////////////////////////////////////////////
// if the join has joinMetaData, then we don't need to process it... unless it needs flipped //
///////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData joinMetaData = queryJoin.getJoinMetaData();
if(joinMetaData != null)
{
boolean isJoinLeftTableInQuery = false;
String joinMetaDataLeftTable = joinMetaData.getLeftTable();
if(joinMetaDataLeftTable.equals(mainTableName))
{
isJoinLeftTableInQuery = true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check the other joins in this query - if any of them have this join's left-table as their baseTable, then set the flag to true //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QueryJoin otherJoin : queryJoins)
{
if(otherJoin == queryJoin)
{
continue;
}
if(Objects.equals(otherJoin.getBaseTableOrAlias(), joinMetaDataLeftTable))
{
isJoinLeftTableInQuery = true;
break;
}
}
/////////////////////////////////////////////////////////////////////////////////
// if the join's left-table isn't in the query, then we need to flip the join. //
/////////////////////////////////////////////////////////////////////////////////
if(!isJoinLeftTableInQuery)
{
log("Flipping queryJoin because its leftTable wasn't found in the query", logPair("joinMetaDataName", joinMetaData.getName()), logPair("leftTable", joinMetaDataLeftTable));
queryJoin.setJoinMetaData(joinMetaData.flip());
}
}
else
{
//////////////////////////////////////////////////////////////////////
// try to find a direct join between the main table and this table. //
@ -190,6 +279,7 @@ public class JoinsContext
QJoinMetaData found = findJoinMetaData(instance, baseTableName, queryJoin.getJoinTable());
if(found != null)
{
log("Found joinMetaData - setting it in queryJoin", logPair("joinMetaDataName", found.getName()), logPair("baseTableName", baseTableName), logPair("joinTable", queryJoin.getJoinTable()));
queryJoin.setJoinMetaData(found);
}
else
@ -197,15 +287,13 @@ public class JoinsContext
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, the join must be indirect - so look for an exposedJoin that will have a joinPath that will connect us //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.debug("Looking for an exposed join...", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()));
QTableMetaData mainTable = instance.getTable(mainTableName);
boolean addedAnyQueryJoins = false;
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(mainTable.getExposedJoins()))
{
if(queryJoin.getJoinTable().equals(exposedJoin.getJoinTable()))
{
LOG.debug("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
log("Found an exposed join", logPair("mainTable", mainTableName), logPair("joinTable", queryJoin.getJoinTable()), logPair("joinPath", exposedJoin.getJoinPath()));
/////////////////////////////////////////////////////////////////////////////////////
// loop backward through the join path (from the joinTable back to the main table) //
@ -250,7 +338,7 @@ public class JoinsContext
QueryJoin queryJoinToAdd = makeQueryJoinFromJoinAndTableNames(nextTable, tmpTable, joinToAdd);
queryJoinToAdd.setType(queryJoin.getType());
addedAnyQueryJoins = true;
this.addQueryJoin(queryJoinToAdd);
this.addQueryJoin(queryJoinToAdd, "forExposedJoin");
}
}
@ -377,9 +465,9 @@ public class JoinsContext
**
** e.g., Given:
** FROM `order` INNER JOIN line_item li
** hasAliasOrTable("order") => true
** hasAliasOrTable("li") => false
** hasAliasOrTable("line_item") => true
** hasTable("order") => true
** hasTable("li") => false
** hasTable("line_item") => true
*******************************************************************************/
public boolean hasTable(String table)
{
@ -415,15 +503,17 @@ public class JoinsContext
for(String filterTable : filterTables)
{
log("Evaluating filterTable", logPair("filterTable", filterTable));
if(!aliasToTableNameMap.containsKey(filterTable) && !Objects.equals(mainTableName, filterTable))
{
log("- table not in query - adding it", logPair("filterTable", filterTable));
boolean found = false;
for(QJoinMetaData join : CollectionUtils.nonNullMap(QContext.getQInstance().getJoins()).values())
{
QueryJoin queryJoin = makeQueryJoinFromJoinAndTableNames(mainTableName, filterTable, join);
if(queryJoin != null)
{
this.addQueryJoin(queryJoin);
this.addQueryJoin(queryJoin, "forFilter (join found in instance)");
found = true;
break;
}
@ -432,7 +522,7 @@ public class JoinsContext
if(!found)
{
QueryJoin queryJoin = new QueryJoin().withJoinTable(filterTable).withType(QueryJoin.Type.INNER);
this.addQueryJoin(queryJoin);
this.addQueryJoin(queryJoin, "forFilter (join not found in instance)");
}
}
}
@ -569,4 +659,14 @@ public class JoinsContext
{
}
/*******************************************************************************
**
*******************************************************************************/
private void log(String message, LogPair... logPairs)
{
LOG.log(logLevel, message, null, logPairs);
}
}

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

@ -50,6 +50,17 @@ public class RenderWidgetInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getActionIdentity()
{
return (getClass().getSimpleName() + ":" + widgetMetaData.getName());
}
/*******************************************************************************
** Getter for widgetMetaData
**

View File

@ -116,16 +116,19 @@ public class AuditsMetaDataProvider
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT_TABLE)
.withTableName(TABLE_NAME_AUDIT_TABLE)
.withOrderByField("name")
);
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT_USER)
.withTableName(TABLE_NAME_AUDIT_USER)
.withOrderByField("name")
);
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT)
.withTableName(TABLE_NAME_AUDIT)
.withOrderByField("id", false)
);
}

View File

@ -28,6 +28,7 @@ import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.savedfilters.SavedFilter;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
@ -50,7 +51,7 @@ public class TableTrigger extends QRecordEntity
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName;
@QField(/* todo possibleValueSourceName = */)
@QField(possibleValueSourceName = SavedFilter.TABLE_NAME)
private Integer filterId;
@QField(possibleValueSourceName = Script.TABLE_NAME)

View File

@ -0,0 +1,45 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*******************************************************************************
** Annotation to place onto fields in a QRecordEntity, to mark them as associated
** record lists
**
*******************************************************************************/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QAssociation
{
/*******************************************************************************
**
*******************************************************************************/
String name();
}

View File

@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.model.data;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedParameterizedType;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.Instant;
import java.time.LocalDate;
@ -40,7 +44,9 @@ import java.util.Optional;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -50,7 +56,8 @@ public abstract class QRecordEntity
{
private static final QLogger LOG = QLogger.getLogger(QRecordEntity.class);
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>();
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityField> fieldMapping = new ListingHash<>();
private static final ListingHash<Class<? extends QRecordEntity>, QRecordEntityAssociation> associationMapping = new ListingHash<>();
private Map<String, Serializable> originalRecordValues;
@ -61,11 +68,23 @@ public abstract class QRecordEntity
**
*******************************************************************************/
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord) throws QException
{
return (QRecordEntity.fromQRecord(c, qRecord, ""));
}
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord - where the fields for
** this entity have the given prefix - e.g., if they were selected as part of a join.
**
*******************************************************************************/
public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecord, String fieldNamePrefix) throws QException
{
try
{
T entity = c.getConstructor().newInstance();
entity.populateFromQRecord(qRecord);
entity.populateFromQRecord(qRecord, fieldNamePrefix);
return (entity);
}
catch(Exception e)
@ -80,19 +99,73 @@ public abstract class QRecordEntity
** Build an entity of this QRecord type from a QRecord
**
*******************************************************************************/
protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord) throws QRuntimeException
protected void populateFromQRecord(QRecord qRecord) throws QRuntimeException
{
populateFromQRecord(qRecord, "");
}
/*******************************************************************************
** Build an entity of this QRecord type from a QRecord - where the fields for
** this entity have the given prefix - e.g., if they were selected as part of a join.
**
*******************************************************************************/
protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord, String fieldNamePrefix) throws QRuntimeException
{
try
{
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
originalRecordValues = new HashMap<>();
if(fieldNamePrefix == null)
{
fieldNamePrefix = "";
}
for(QRecordEntityField qRecordEntityField : fieldList)
{
Serializable value = qRecord.getValue(qRecordEntityField.getFieldName());
Serializable value = qRecord.getValue(fieldNamePrefix + qRecordEntityField.getFieldName());
Object typedValue = qRecordEntityField.convertValueType(value);
qRecordEntityField.getSetter().invoke(this, typedValue);
originalRecordValues.put(qRecordEntityField.getFieldName(), value);
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
if(associatedRecords == null)
{
qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
}
else
{
List<QRecordEntity> associatedEntityList = new ArrayList<>();
for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
{
associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
}
qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
}
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<QRecord> associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name());
if(associatedRecords == null)
{
qRecordEntityAssociation.getSetter().invoke(this, (Object) null);
}
else
{
List<QRecordEntity> associatedEntityList = new ArrayList<>();
for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords))
{
associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord));
}
qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList);
}
}
}
catch(Exception e)
{
@ -112,12 +185,30 @@ public abstract class QRecordEntity
{
QRecord qRecord = new QRecord();
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
for(QRecordEntityField qRecordEntityField : fieldList)
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
{
qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
/////////////////////////////////////////////////////////////////////////////////
// do this so an empty list in the entity becomes an empty list in the QRecord //
/////////////////////////////////////////////////////////////////////////////////
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
}
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
{
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
}
}
return (qRecord);
}
catch(Exception e)
@ -127,7 +218,6 @@ public abstract class QRecordEntity
}
/*******************************************************************************
**
*******************************************************************************/
@ -137,8 +227,7 @@ public abstract class QRecordEntity
{
QRecord qRecord = new QRecord();
List<QRecordEntityField> fieldList = getFieldList(this.getClass());
for(QRecordEntityField qRecordEntityField : fieldList)
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
{
Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this);
Serializable originalValue = null;
@ -153,6 +242,25 @@ public abstract class QRecordEntity
}
}
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
{
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
if(associatedEntities != null)
{
/////////////////////////////////////////////////////////////////////////////////
// do this so an empty list in the entity becomes an empty list in the QRecord //
/////////////////////////////////////////////////////////////////////////////////
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
}
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
{
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
}
}
return (qRecord);
}
catch(Exception e)
@ -181,7 +289,15 @@ public abstract class QRecordEntity
{
String fieldName = getFieldNameFromGetter(possibleGetter);
Optional<QField> fieldAnnotation = getQFieldAnnotation(c, fieldName);
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
if(fieldAnnotation.isPresent())
{
fieldList.add(new QRecordEntityField(fieldName, possibleGetter, setter.get(), possibleGetter.getReturnType(), fieldAnnotation.orElse(null)));
}
else
{
LOG.debug("Skipping field without @QField annotation", logPair("class", c.getSimpleName()), logPair("fieldName", fieldName));
}
}
else
{
@ -196,15 +312,73 @@ public abstract class QRecordEntity
/*******************************************************************************
**
*******************************************************************************/
public static List<QRecordEntityAssociation> getAssociationList(Class<? extends QRecordEntity> c)
{
if(!associationMapping.containsKey(c))
{
List<QRecordEntityAssociation> associationList = new ArrayList<>();
for(Method possibleGetter : c.getMethods())
{
if(isGetter(possibleGetter))
{
Optional<Method> setter = getSetterForGetter(c, possibleGetter);
if(setter.isPresent())
{
String fieldName = getFieldNameFromGetter(possibleGetter);
Optional<QAssociation> associationAnnotation = getQAssociationAnnotation(c, fieldName);
if(associationAnnotation.isPresent())
{
Class<? extends QRecordEntity> listTypeParam = (Class<? extends QRecordEntity>) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType());
associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null)));
}
}
else
{
LOG.info("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter.");
}
}
}
associationMapping.put(c, associationList);
}
return (associationMapping.get(c));
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<QField> getQFieldAnnotation(Class<? extends QRecordEntity> c, String fieldName)
{
return (getAnnotationOnField(c, QField.class, fieldName));
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<QAssociation> getQAssociationAnnotation(Class<? extends QRecordEntity> c, String fieldName)
{
return (getAnnotationOnField(c, QAssociation.class, fieldName));
}
/*******************************************************************************
**
*******************************************************************************/
public static <A extends Annotation> Optional<A> getAnnotationOnField(Class<? extends QRecordEntity> c, Class<A> annotationClass, String fieldName)
{
try
{
Field field = c.getDeclaredField(fieldName);
return (Optional.ofNullable(field.getAnnotation(QField.class)));
return (Optional.ofNullable(field.getAnnotation(annotationClass)));
}
catch(NoSuchFieldException e)
{
@ -239,7 +413,7 @@ public abstract class QRecordEntity
{
if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*"))
{
if(isSupportedFieldType(method.getReturnType()))
if(isSupportedFieldType(method.getReturnType()) || isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType()))
{
return (true);
}
@ -247,7 +421,7 @@ public abstract class QRecordEntity
{
if(!method.getName().equals("getClass"))
{
LOG.info("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
LOG.debug("Method [" + method.getName() + "] looks like a getter, but its return type, [" + method.getReturnType() + "], isn't supported.");
}
}
}
@ -304,4 +478,41 @@ public abstract class QRecordEntity
/////////////////////////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean isSupportedAssociation(Class<?> returnType, AnnotatedType annotatedType)
{
Class<?> listTypeParam = getListTypeParam(returnType, annotatedType);
return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam));
}
/*******************************************************************************
**
*******************************************************************************/
private static Class<?> getListTypeParam(Class<?> listType, AnnotatedType annotatedType)
{
if(listType.equals(List.class))
{
if(annotatedType instanceof AnnotatedParameterizedType apt)
{
AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments();
for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments)
{
Type type = annotatedActualTypeArgument.getType();
if(type instanceof Class<?> c)
{
return (c);
}
}
}
}
return (null);
}
}

View File

@ -0,0 +1,110 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.data;
import java.lang.reflect.Method;
/*******************************************************************************
** Reflective information about an association in a QRecordEntity
*******************************************************************************/
public class QRecordEntityAssociation
{
private final String fieldName;
private final Method getter;
private final Method setter;
private final Class<? extends QRecordEntity> associatedType;
private final QAssociation associationAnnotation;
/*******************************************************************************
** Constructor.
*******************************************************************************/
public QRecordEntityAssociation(String fieldName, Method getter, Method setter, Class<? extends QRecordEntity> associatedType, QAssociation associationAnnotation)
{
this.fieldName = fieldName;
this.getter = getter;
this.setter = setter;
this.associatedType = associatedType;
this.associationAnnotation = associationAnnotation;
}
/*******************************************************************************
** Getter for fieldName
**
*******************************************************************************/
public String getFieldName()
{
return fieldName;
}
/*******************************************************************************
** Getter for getter
**
*******************************************************************************/
public Method getGetter()
{
return getter;
}
/*******************************************************************************
** Getter for setter
**
*******************************************************************************/
public Method getSetter()
{
return setter;
}
/*******************************************************************************
** Getter for associatedType
**
*******************************************************************************/
public Class<? extends QRecordEntity> getAssociatedType()
{
return associatedType;
}
/*******************************************************************************
** Getter for associationAnnotation
**
*******************************************************************************/
public QAssociation getAssociationAnnotation()
{
return associationAnnotation;
}
}

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

@ -22,7 +22,6 @@
package com.kingsrook.qqq.backend.core.model.metadata;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@ -38,17 +37,24 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
**
*******************************************************************************/
@JsonDeserialize(using = QBackendMetaDataDeserializer.class)
public class QBackendMetaData
public class QBackendMetaData implements TopLevelMetaDataInterface
{
private String name;
private String backendType;
private Boolean usesVariants = false;
private String variantOptionsTableName;
private Set<Capability> enabledCapabilities = new HashSet<>();
private Set<Capability> disabledCapabilities = new HashSet<>();
private Boolean usesVariants = false;
private String variantOptionsTableIdField;
private String variantOptionsTableNameField;
private String variantOptionsTableTypeField;
private String variantOptionsTableTypeValue;
private String variantOptionsTableUsernameField;
private String variantOptionsTablePasswordField;
private String variantOptionsTableApiKeyField;
private String variantOptionsTableName;
// todo - at some point, we may want to apply this to secret properties on subclasses?
// @JsonFilter("secretsFilter")
@ -59,6 +65,10 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData()
{
/////////////////////////////////////////////////////////////////////////////
// by default, we will turn off the query stats capability on all backends //
/////////////////////////////////////////////////////////////////////////////
withoutCapability(Capability.QUERY_STATS);
}
@ -199,6 +209,10 @@ public class QBackendMetaData
public void setEnabledCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
if(this.disabledCapabilities != null)
{
this.disabledCapabilities.removeAll(enabledCapabilities);
}
}
@ -209,7 +223,7 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData withEnabledCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
setEnabledCapabilities(enabledCapabilities);
return (this);
}
@ -221,7 +235,10 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData withCapabilities(Set<Capability> enabledCapabilities)
{
this.enabledCapabilities = enabledCapabilities;
for(Capability enabledCapability : enabledCapabilities)
{
withCapability(enabledCapability);
}
return (this);
}
@ -238,6 +255,7 @@ public class QBackendMetaData
this.enabledCapabilities = new HashSet<>();
}
this.enabledCapabilities.add(capability);
this.disabledCapabilities.remove(capability);
return (this);
}
@ -249,11 +267,10 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData withCapabilities(Capability... enabledCapabilities)
{
if(this.enabledCapabilities == null)
for(Capability enabledCapability : enabledCapabilities)
{
this.enabledCapabilities = new HashSet<>();
withCapability(enabledCapability);
}
this.enabledCapabilities.addAll(Arrays.stream(enabledCapabilities).toList());
return (this);
}
@ -277,6 +294,10 @@ public class QBackendMetaData
public void setDisabledCapabilities(Set<Capability> disabledCapabilities)
{
this.disabledCapabilities = disabledCapabilities;
if(this.enabledCapabilities != null)
{
this.enabledCapabilities.removeAll(disabledCapabilities);
}
}
@ -287,7 +308,7 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData withDisabledCapabilities(Set<Capability> disabledCapabilities)
{
this.disabledCapabilities = disabledCapabilities;
setDisabledCapabilities(disabledCapabilities);
return (this);
}
@ -299,11 +320,10 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData withoutCapabilities(Capability... disabledCapabilities)
{
if(this.disabledCapabilities == null)
for(Capability disabledCapability : disabledCapabilities)
{
this.disabledCapabilities = new HashSet<>();
withoutCapability(disabledCapability);
}
this.disabledCapabilities.addAll(Arrays.stream(disabledCapabilities).toList());
return (this);
}
@ -315,7 +335,10 @@ public class QBackendMetaData
*******************************************************************************/
public QBackendMetaData withoutCapabilities(Set<Capability> disabledCapabilities)
{
this.disabledCapabilities = disabledCapabilities;
for(Capability disabledCapability : disabledCapabilities)
{
withCapability(disabledCapability);
}
return (this);
}
@ -332,6 +355,7 @@ public class QBackendMetaData
this.disabledCapabilities = new HashSet<>();
}
this.disabledCapabilities.add(capability);
this.enabledCapabilities.remove(capability);
return (this);
}
@ -381,7 +405,224 @@ public class QBackendMetaData
/*******************************************************************************
** Getter for variantsOptionTableName
** Getter for variantOptionsTableIdField
*******************************************************************************/
public String getVariantOptionsTableIdField()
{
return (this.variantOptionsTableIdField);
}
/*******************************************************************************
** Setter for variantOptionsTableIdField
*******************************************************************************/
public void setVariantOptionsTableIdField(String variantOptionsTableIdField)
{
this.variantOptionsTableIdField = variantOptionsTableIdField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableIdField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableIdField(String variantOptionsTableIdField)
{
this.variantOptionsTableIdField = variantOptionsTableIdField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableNameField
*******************************************************************************/
public String getVariantOptionsTableNameField()
{
return (this.variantOptionsTableNameField);
}
/*******************************************************************************
** Setter for variantOptionsTableNameField
*******************************************************************************/
public void setVariantOptionsTableNameField(String variantOptionsTableNameField)
{
this.variantOptionsTableNameField = variantOptionsTableNameField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableNameField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableNameField(String variantOptionsTableNameField)
{
this.variantOptionsTableNameField = variantOptionsTableNameField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableTypeField
*******************************************************************************/
public String getVariantOptionsTableTypeField()
{
return (this.variantOptionsTableTypeField);
}
/*******************************************************************************
** Setter for variantOptionsTableTypeField
*******************************************************************************/
public void setVariantOptionsTableTypeField(String variantOptionsTableTypeField)
{
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableTypeField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableTypeField(String variantOptionsTableTypeField)
{
this.variantOptionsTableTypeField = variantOptionsTableTypeField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableTypeValue
*******************************************************************************/
public String getVariantOptionsTableTypeValue()
{
return (this.variantOptionsTableTypeValue);
}
/*******************************************************************************
** Setter for variantOptionsTableTypeValue
*******************************************************************************/
public void setVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
{
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableTypeValue
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableTypeValue(String variantOptionsTableTypeValue)
{
this.variantOptionsTableTypeValue = variantOptionsTableTypeValue;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableUsernameField
*******************************************************************************/
public String getVariantOptionsTableUsernameField()
{
return (this.variantOptionsTableUsernameField);
}
/*******************************************************************************
** Setter for variantOptionsTableUsernameField
*******************************************************************************/
public void setVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
{
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableUsernameField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableUsernameField(String variantOptionsTableUsernameField)
{
this.variantOptionsTableUsernameField = variantOptionsTableUsernameField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTablePasswordField
*******************************************************************************/
public String getVariantOptionsTablePasswordField()
{
return (this.variantOptionsTablePasswordField);
}
/*******************************************************************************
** Setter for variantOptionsTablePasswordField
*******************************************************************************/
public void setVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
{
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTablePasswordField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTablePasswordField(String variantOptionsTablePasswordField)
{
this.variantOptionsTablePasswordField = variantOptionsTablePasswordField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableApiKeyField
*******************************************************************************/
public String getVariantOptionsTableApiKeyField()
{
return (this.variantOptionsTableApiKeyField);
}
/*******************************************************************************
** Setter for variantOptionsTableApiKeyField
*******************************************************************************/
public void setVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
{
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
}
/*******************************************************************************
** Fluent setter for variantOptionsTableApiKeyField
*******************************************************************************/
public QBackendMetaData withVariantOptionsTableApiKeyField(String variantOptionsTableApiKeyField)
{
this.variantOptionsTableApiKeyField = variantOptionsTableApiKeyField;
return (this);
}
/*******************************************************************************
** Getter for variantOptionsTableName
*******************************************************************************/
public String getVariantOptionsTableName()
{
@ -391,7 +632,7 @@ public class QBackendMetaData
/*******************************************************************************
** Setter for variantsOptionTableName
** Setter for variantOptionsTableName
*******************************************************************************/
public void setVariantOptionsTableName(String variantOptionsTableName)
{
@ -401,12 +642,22 @@ public class QBackendMetaData
/*******************************************************************************
** Fluent setter for variantsOptionTableName
** Fluent setter for variantOptionsTableName
*******************************************************************************/
public QBackendMetaData withVariantsOptionTableName(String variantsOptionTableName)
public QBackendMetaData withVariantOptionsTableName(String variantOptionsTableName)
{
this.variantOptionsTableName = variantsOptionTableName;
this.variantOptionsTableName = variantOptionsTableName;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addSelfToInstance(QInstance qInstance)
{
qInstance.addBackend(this);
}
}

View File

@ -91,8 +91,9 @@ public class QInstance
private Map<String, QQueueProviderMetaData> queueProviders = new LinkedHashMap<>();
private Map<String, QQueueMetaData> queues = new LinkedHashMap<>();
private Map<String, QMiddlewareInstanceMetaData> middlewareMetaData = new LinkedHashMap<>();
private Map<String, QSupplementalInstanceMetaData> supplementalMetaData = new LinkedHashMap<>();
private String deploymentMode;
private Map<String, String> environmentValues = new LinkedHashMap<>();
private String defaultTimeZoneId = "UTC";
@ -1083,60 +1084,60 @@ public class QInstance
/*******************************************************************************
** Getter for middlewareMetaData
** Getter for supplementalMetaData
*******************************************************************************/
public Map<String, QMiddlewareInstanceMetaData> getMiddlewareMetaData()
public Map<String, QSupplementalInstanceMetaData> getSupplementalMetaData()
{
return (this.middlewareMetaData);
return (this.supplementalMetaData);
}
/*******************************************************************************
** Getter for middlewareMetaData
** Getter for supplementalMetaData
*******************************************************************************/
public QMiddlewareInstanceMetaData getMiddlewareMetaData(String type)
public QSupplementalInstanceMetaData getSupplementalMetaData(String type)
{
if(this.middlewareMetaData == null)
if(this.supplementalMetaData == null)
{
return (null);
}
return this.middlewareMetaData.get(type);
return this.supplementalMetaData.get(type);
}
/*******************************************************************************
** Setter for middlewareMetaData
** Setter for supplementalMetaData
*******************************************************************************/
public void setMiddlewareMetaData(Map<String, QMiddlewareInstanceMetaData> middlewareMetaData)
public void setSupplementalMetaData(Map<String, QSupplementalInstanceMetaData> supplementalMetaData)
{
this.middlewareMetaData = middlewareMetaData;
this.supplementalMetaData = supplementalMetaData;
}
/*******************************************************************************
** Fluent setter for middlewareMetaData
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QInstance withMiddlewareMetaData(Map<String, QMiddlewareInstanceMetaData> middlewareMetaData)
public QInstance withSupplementalMetaData(Map<String, QSupplementalInstanceMetaData> supplementalMetaData)
{
this.middlewareMetaData = middlewareMetaData;
this.supplementalMetaData = supplementalMetaData;
return (this);
}
/*******************************************************************************
** Fluent setter for middlewareMetaData
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QInstance withMiddlewareMetaData(QMiddlewareInstanceMetaData middlewareMetaData)
public QInstance withSupplementalMetaData(QSupplementalInstanceMetaData supplementalMetaData)
{
if(this.middlewareMetaData == null)
if(this.supplementalMetaData == null)
{
this.middlewareMetaData = new HashMap<>();
this.supplementalMetaData = new HashMap<>();
}
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
return (this);
}
@ -1165,4 +1166,36 @@ public class QInstance
}
this.joinGraph = joinGraph;
}
/*******************************************************************************
** Getter for deploymentMode
*******************************************************************************/
public String getDeploymentMode()
{
return (this.deploymentMode);
}
/*******************************************************************************
** Setter for deploymentMode
*******************************************************************************/
public void setDeploymentMode(String deploymentMode)
{
this.deploymentMode = deploymentMode;
}
/*******************************************************************************
** Fluent setter for deploymentMode
*******************************************************************************/
public QInstance withDeploymentMode(String deploymentMode)
{
this.deploymentMode = deploymentMode;
return (this);
}
}

View File

@ -27,42 +27,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Base-class for instance-level meta-data defined for a specific middleware.
** Base-class for instance-level meta-data defined by some supplemental module, etc,
** outside of qqq core
*******************************************************************************/
public abstract class QMiddlewareInstanceMetaData
public abstract class QSupplementalInstanceMetaData
{
protected String type;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QMiddlewareInstanceMetaData withType(String type)
{
this.type = type;
return (this);
}
public abstract String getType();

View File

@ -60,7 +60,6 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
private String auth0ClientSecretField;
private Serializable qqqRecordIdField;
/////////////////////////////////////
// fields on the accessToken table //
/////////////////////////////////////

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

@ -85,7 +85,7 @@ public class QFieldMetaData implements Cloneable
private List<FieldAdornment> adornments;
private Map<String, QMiddlewareFieldMetaData> middlewareMetaData;
private Map<String, QSupplementalFieldMetaData> supplementalMetaData;
@ -840,60 +840,60 @@ public class QFieldMetaData implements Cloneable
/*******************************************************************************
** Getter for middlewareMetaData
** Getter for supplementalMetaData
*******************************************************************************/
public Map<String, QMiddlewareFieldMetaData> getMiddlewareMetaData()
public Map<String, QSupplementalFieldMetaData> getSupplementalMetaData()
{
return (this.middlewareMetaData);
return (this.supplementalMetaData);
}
/*******************************************************************************
** Getter for middlewareMetaData
** Getter for supplementalMetaData
*******************************************************************************/
public QMiddlewareFieldMetaData getMiddlewareMetaData(String type)
public QSupplementalFieldMetaData getSupplementalMetaData(String type)
{
if(this.middlewareMetaData == null)
if(this.supplementalMetaData == null)
{
return (null);
}
return this.middlewareMetaData.get(type);
return this.supplementalMetaData.get(type);
}
/*******************************************************************************
** Setter for middlewareMetaData
** Setter for supplementalMetaData
*******************************************************************************/
public void setMiddlewareMetaData(Map<String, QMiddlewareFieldMetaData> middlewareMetaData)
public void setSupplementalMetaData(Map<String, QSupplementalFieldMetaData> supplementalMetaData)
{
this.middlewareMetaData = middlewareMetaData;
this.supplementalMetaData = supplementalMetaData;
}
/*******************************************************************************
** Fluent setter for middlewareMetaData
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QFieldMetaData withMiddlewareMetaData(Map<String, QMiddlewareFieldMetaData> middlewareMetaData)
public QFieldMetaData withSupplementalMetaData(Map<String, QSupplementalFieldMetaData> supplementalMetaData)
{
this.middlewareMetaData = middlewareMetaData;
this.supplementalMetaData = supplementalMetaData;
return (this);
}
/*******************************************************************************
** Fluent setter for middlewareMetaData
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QFieldMetaData withMiddlewareMetaData(QMiddlewareFieldMetaData middlewareMetaData)
public QFieldMetaData withSupplementalMetaData(QSupplementalFieldMetaData supplementalMetaData)
{
if(this.middlewareMetaData == null)
if(this.supplementalMetaData == null)
{
this.middlewareMetaData = new HashMap<>();
this.supplementalMetaData = new HashMap<>();
}
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
return (this);
}

View File

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

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

@ -77,6 +77,21 @@ public class QPossibleValueSource implements TopLevelMetaDataInterface
/*******************************************************************************
** Create a new possible value source, for a table, with default settings.
** e.g., name & table name from the tableName parameter; type=TABLE; and LABEL_ONLY format
*******************************************************************************/
public static QPossibleValueSource newForTable(String tableName)
{
return new QPossibleValueSource()
.withName(tableName)
.withType(QPossibleValueSourceType.TABLE)
.withTableName(tableName)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -54,6 +54,9 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private BasepullConfiguration basepullConfiguration;
private QPermissionRules permissionRules;
private Integer minInputRecords = null;
private Integer maxInputRecords = null;
private List<QStepMetaData> stepList; // these are the steps that are ran, by-default, in the order they are ran in
private Map<String, QStepMetaData> steps; // this is the full map of possible steps
@ -61,6 +64,8 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
private QScheduleMetaData schedule;
private Map<String, QSupplementalProcessMetaData> supplementalMetaData;
/*******************************************************************************
@ -544,4 +549,126 @@ public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissi
qInstance.addProcess(this);
}
/*******************************************************************************
** Getter for supplementalMetaData
*******************************************************************************/
public Map<String, QSupplementalProcessMetaData> getSupplementalMetaData()
{
return (this.supplementalMetaData);
}
/*******************************************************************************
** Getter for supplementalMetaData
*******************************************************************************/
public QSupplementalProcessMetaData getSupplementalMetaData(String type)
{
if(this.supplementalMetaData == null)
{
return (null);
}
return this.supplementalMetaData.get(type);
}
/*******************************************************************************
** Setter for supplementalMetaData
*******************************************************************************/
public void setSupplementalMetaData(Map<String, QSupplementalProcessMetaData> supplementalMetaData)
{
this.supplementalMetaData = supplementalMetaData;
}
/*******************************************************************************
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QProcessMetaData withSupplementalMetaData(Map<String, QSupplementalProcessMetaData> supplementalMetaData)
{
this.supplementalMetaData = supplementalMetaData;
return (this);
}
/*******************************************************************************
** Fluent setter for supplementalMetaData
*******************************************************************************/
public QProcessMetaData withSupplementalMetaData(QSupplementalProcessMetaData supplementalMetaData)
{
if(this.supplementalMetaData == null)
{
this.supplementalMetaData = new HashMap<>();
}
this.supplementalMetaData.put(supplementalMetaData.getType(), supplementalMetaData);
return (this);
}
/*******************************************************************************
** Getter for minInputRecords
*******************************************************************************/
public Integer getMinInputRecords()
{
return (this.minInputRecords);
}
/*******************************************************************************
** Setter for minInputRecords
*******************************************************************************/
public void setMinInputRecords(Integer minInputRecords)
{
this.minInputRecords = minInputRecords;
}
/*******************************************************************************
** Fluent setter for minInputRecords
*******************************************************************************/
public QProcessMetaData withMinInputRecords(Integer minInputRecords)
{
this.minInputRecords = minInputRecords;
return (this);
}
/*******************************************************************************
** Getter for maxInputRecords
*******************************************************************************/
public Integer getMaxInputRecords()
{
return (this.maxInputRecords);
}
/*******************************************************************************
** Setter for maxInputRecords
*******************************************************************************/
public void setMaxInputRecords(Integer maxInputRecords)
{
this.maxInputRecords = maxInputRecords;
}
/*******************************************************************************
** Fluent setter for maxInputRecords
*******************************************************************************/
public QProcessMetaData withMaxInputRecords(Integer maxInputRecords)
{
this.maxInputRecords = maxInputRecords;
return (this);
}
}

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