Compare commits

...

805 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
ceed7081ca Merge branch 'release/0.14.0' 2023-06-08 14:25:38 -05:00
78ba2b591c Update for next development version 2023-06-08 14:22:52 -05:00
a4aeafdf91 Update versions for release 2023-06-08 14:22:14 -05:00
f47ae7f1fa Merge branch 'integration/sprint-26' into dev 2023-06-08 12:35:35 -05:00
fcf452836b Enrich api field name to label if missing 2023-06-06 19:27:33 -05:00
3b398942e7 Pass original records in as oldRecords for UpdateAction.performValidations 2023-06-06 19:25:05 -05:00
b52a014154 Don't try to manageAssociations for a record that had errors 2023-06-06 19:24:40 -05:00
e97ca5b5c5 Fix usage of subfilters in automation actions 2023-06-06 13:31:25 -05:00
a117c6ff3f Merge branch 'feature/CTLE-435-review-and-complete-parcel-sla' into integration/sprint-26 2023-06-06 11:15:11 -05:00
c08856a92c Merge branch 'feature/CTLE-433-cart-rover-now-extensiv-integration' into integration/sprint-26 2023-06-06 11:14:58 -05:00
c5b14cd22c Give explicit error if table doesn't have a primary key 2023-06-06 11:06:57 -05:00
8de9288c05 Fix merge conflict 2023-06-06 09:59:44 -05:00
9b5d1e1208 Merge branch 'feature/CTLE-153-default-ct-live-packing-slips-to-deposco' into integration/sprint-26
# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
#	qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
#	qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
#	qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java
#	qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
2023-06-06 09:58:12 -05:00
f5047e5f50 Merge branch 'dev' into feature/CTLE-434-oms-update-business-logic 2023-06-06 09:06:34 -05:00
12dd3617e5 Add a few more known http status for outbound status codes 2023-06-05 16:50:41 -05:00
ba544adb76 Read filter & fields from formParam if not found as queryParam, in exports 2023-06-05 16:48:35 -05:00
c91a678905 More control options around javalin access logging 2023-06-05 14:49:05 -05:00
f1ebff28eb updated shouldBeRetryableServerErrorException in base api action utils to only retry on 500 errors if query or get actions 2023-06-05 11:19:56 -05:00
e4ae717f7f fix missing import 2023-06-05 08:30:04 -05:00
fdbc751048 more support for blobs
- fetch heavy flag for process extract
- ignore blobs in bulk edit, load
- set download urls in child-list render and javalin via shared method in QValueFormatter

also, make ETL load step aware of transform step, and more flexible process summary between them
2023-06-05 08:17:45 -05:00
c67d042e26 Add null check 2023-06-02 11:34:18 -05:00
a4bd3b56f6 Fix comments, cleanup from code review 2023-06-02 09:41:07 -05:00
5330f3de90 Fix merge conflict in update method 2023-06-02 09:04:16 -05:00
eca176a7f0 Merge branch 'dev' into feature/CTLE-434-oms-update-business-logic
# Conflicts:
#	qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java
#	qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
2023-06-02 09:02:05 -05:00
0b525f8775 Checkpoint - query stats (plus recordEntities with associations) 2023-06-02 08:58:24 -05:00
499d59ade2 Add overload of loadTableToMap that takes Consumer<QueryInput> (e.g., to request heavy fields) 2023-06-01 16:32:34 -05:00
b67813b7ad Update test to handle blob correctly 2023-06-01 16:31:54 -05:00
7634246a03 Handling of BLOBs 2023-06-01 16:13:02 -05:00
1db6dfb2ad avoid calling auditDetail insert if no records 2023-06-01 15:03:21 -05:00
f5516c8122 Make column stats treat "" and null the same (as a non-value or blank) 2023-06-01 15:03:21 -05:00
6455171cee removed unused subclass 2023-05-30 14:57:34 -05:00
ca1cc853fc fixed access on subclass method in test 2023-05-30 14:55:56 -05:00
c78d598035 Add an eventCartridge (handler) for methods that throw; add template identifier (a name) to input 2023-05-30 14:05:00 -05:00
a5387ff9db added retryable exception and method that can be overridden in base classes to allow retrying in different circumstances, added SC_GATEWAY_TIMEOUT as a logger.info 2023-05-30 14:04:30 -05:00
794fb5e87a Expand javalin tests 2023-05-30 13:48:26 -05:00
343f3fe01a Update to support both JSON and multipart form bodies for create and update. 2023-05-30 11:20:49 -05:00
364e9f420b Update to support both JSON and multipart form bodies for create and update. 2023-05-30 11:13:20 -05:00
a75530b466 Updates for more heavy-field handling 2023-05-30 10:13:04 -05:00
489f12996d updated to allow 'trying again' when server side 500 error occur in makeRequest() 2023-05-25 11:34:50 -05:00
6b1e3aa572 Merge branch 'dev' into feature/CTLE-434-oms-update-business-logic
# Conflicts:
#	qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
2023-05-25 11:08:31 -05:00
8235bb2bb7 Merge branch 'dev' into feature/CTLE-153-default-ct-live-packing-slips-to-deposco 2023-05-25 10:57:51 -05:00
5185758855 add check for "snapshot-BRANCH_SLUG" arg to update-all-qqq-deps.sh -- if given, then we look for branchy versions, else we use main versions [skip ci] 2023-05-25 10:55:10 -05:00
515e04ecfe Update api json mapping to include null & empty values 2023-05-25 10:22:04 -05:00
76b102b811 updated to still log info on api gateway error, but still throw exception 2023-05-24 21:28:52 -05:00
36efc4c2d9 More BLOB; FILE_DOWNLOAD adornment; javalin field download endpoint 2023-05-24 17:07:19 -05:00
6ca06bf49d CTLE-435: removed unused input param from html renderer methods 2023-05-24 11:05:59 -05:00
614aead348 add more support for byte[]/BLOB field type; checkpoint 2023-05-23 14:07:36 -05:00
de74455de7 more changes to help reduce warnings that should probably be infos in loggly 2023-05-23 10:11:54 -05:00
7491e5f819 api association fixes; mostly about propagating ids/fkeys, and having fields (in maps) as expected field types (cherry pick 74d003ed) 2023-05-23 09:00:41 -05:00
74d003ed3c api association fixes; mostly about propagating ids/fkeys, and having fields (in maps) as expected field types 2023-05-22 16:05:34 -05:00
4b6b60f331 Add concept of inputSource on insert/update/delete actions. 2023-05-19 16:34:47 -05:00
e10af188ef Add concept of inputSource on insert/update/delete actions. 2023-05-19 16:34:26 -05:00
486a942fdc Merge remote-tracking branch 'origin/dev' into feature/CTLE-434-oms-update-business-logic 2023-05-19 14:57:51 -05:00
9dc6f4ccf8 Add DEFAULT_PREVIEW_MESSAGE_PREFIX 2023-05-19 08:36:19 -05:00
1161e65c03 Add showReloadButton, showExportButton 2023-05-19 08:35:31 -05:00
fd550bad85 Add method icon(name, color) 2023-05-19 08:35:09 -05:00
5bd1fd4a7f Add environmentBannerText and environmentBannerColor 2023-05-18 14:17:59 -05:00
46afb46910 Add log info for committing of slow transactions 2023-05-18 09:49:57 -05:00
7e1a7c7fd7 CTLE-433: updates to support extensiv integration 2023-05-17 20:57:03 -05:00
274567aefa Merge branch 'dev' into feature/CTLE-434-oms-update-business-logic 2023-05-16 19:59:15 -05:00
c0237e9d09 Merge pull request #18 from Kingsrook/hotfix/fix-cache-bugs
hotfix: attempt to remove some caching bugs when updating old cache r…
2023-05-16 13:59:55 -05:00
3f9370e9b5 hotfix: updated to null out the getoutput's record if it was not returned by the original source 2023-05-16 12:02:47 -05:00
01d3937889 hotfix: attempt to remove some caching bugs when updating old cache records and we got uncachable errors 2023-05-16 11:38:24 -05:00
caaf76c9a5 Add placeholder lines for uniqueKey, recordSecurityLock, and auditRules 2023-05-16 08:32:46 -05:00
0e60811c43 Fixed previous fix (was adding wrong queryJoin in exposedJoins loop else block) 2023-05-16 08:32:02 -05:00
4eb28cd1b7 Fix for table being added to query twice, if it's added for security, and then for being in a where clause. 2023-05-15 15:08:56 -05:00
8470da40f1 Checkpoitn 2023-05-15 12:14:27 -05:00
dd63e8d4e2 Initial checkin 2023-05-12 16:48:27 -05:00
298e73e144 Don't try to manage associations of empty lists 2023-05-12 16:40:50 -05:00
4e5fd62808 Merge branch 'dev' into feature/CTLE-434-oms-update-business-logic 2023-05-12 14:57:51 -05:00
14fc7b0ba8 Add criteria operator NOT_EQUALS_OR_IS_NULL 2023-05-12 14:50:27 -05:00
676783fdf5 Add toQRecordOnlyChangedFields 2023-05-12 12:21:59 -05:00
3e7684bb8d Don't audit for records that failed their DML 2023-05-12 12:21:34 -05:00
815bd8b0ce Updates to work with branch-specific maven deployments in/with circleci 2023-05-11 10:15:40 -05:00
7a5124ae06 Reset the input list of primary keys - callers may expect that! 2023-05-10 17:59:25 -05:00
e9328c6653 Make list of primary keys explicitly serializble, so when they change to field's type later, it doesn't blow up (should we fix that on the other side? (yes)) 2023-05-10 17:50:09 -05:00
1121dc14d4 Fixed processing output records (those without errors aren't present in map) 2023-05-10 17:40:07 -05:00
0b996ef008 CTLE-433: attempt to get over 80% coverage in javalin 2023-05-10 17:31:20 -05:00
d6acb0c1ef you see, checkstyle was disabled in intellij, for reasons 2023-05-10 17:10:04 -05:00
fb99c615fe Added javadoc explaining how doQuery fetches many pages up to limit 2023-05-10 17:05:12 -05:00
eef5936282 bulk insert & delete w/ pre-validation and warnings and errors and such 2023-05-10 17:04:57 -05:00
e66b649699 CTLE-433: fixed more tests due to new data 2023-05-10 16:35:00 -05:00
ef8db2786d CTLE-433: added boolean type to deserializer 2023-05-10 16:23:48 -05:00
e06a5ab4b3 CTLE-433: checkpoint commit of backend variants, updated process utils to no longer take in input object since now comes from qContext, put instruction coverage back to 80% 2023-05-10 15:50:03 -05:00
b9ad0e7e21 Warnings & errors on update 2023-05-10 10:10:14 -05:00
33555701a4 Updates to allow validations on bulk-edit, with warnings and errors coming back on review & result screens. 2023-05-10 10:09:36 -05:00
88e24a08fc Updates to work with branch-specific maven deployments in/with circleci 2023-05-09 13:00:49 -05:00
2c7919abce Update to look at tag name too 2023-05-09 12:27:03 -05:00
21251b8d9b attempt to dploy branches to unique verion name 2023-05-09 10:43:23 -05:00
b412a424ca Fix delete test for error handling from customizers 2023-05-09 10:24:29 -05:00
7af164e002 More error handling from customizers 2023-05-09 10:09:29 -05:00
b2c7062709 Convert QRecord errors and warnings to new QStatusMessage type hierarchy. 2023-05-09 08:49:46 -05:00
cedc1edfac Add queryJoins to access log if-slow 2023-05-08 15:27:57 -05:00
c75d19d72a Add possible value 422... would be nice to get these all from a lib... 2023-05-08 15:27:40 -05:00
647c5968d3 Updated expected type on post-read customizer 2023-05-08 15:24:49 -05:00
db770c7e03 Fixed pre-delete warning check 2023-05-08 15:14:03 -05:00
265847e01a Completed first round implementation of {pre,post}{insert,delete} actions 2023-05-08 15:06:31 -05:00
6aef4d92e8 Merge branch 'integration/sprint-26' into feature/CTLE-434-oms-update-business-logic 2023-05-08 11:29:21 -05:00
1e1a33c250 fixed billing dashboard links and made them permissed 2023-05-08 11:25:08 -05:00
0fa418496a Merge branch 'dev' of github.com:Kingsrook/qqq into dev 2023-05-08 11:22:15 -05:00
d39698740c Removing TableCustomizer (mostly redundant with TableCustomizers and confusing for no real gain). Initial pass at update, delete customizers 2023-05-05 17:00:47 -05:00
036b7dc115 Refactor to get rid of Usage parameter in QCodeReference 2023-05-05 16:59:52 -05:00
cc765c66d6 Initial checkin - WIP 2023-05-05 16:57:54 -05:00
6acf0bf93a Increasing size of api log fields (mediumtext, 16MB) 2023-05-05 15:39:42 -05:00
16d54aa81f Merge branch 'integration/sprint-26' into dev 2023-05-05 13:12:04 -05:00
a1b96c6cee updated to hide hidden fields in frontentmetadata, updated error responses 2023-05-05 13:05:35 -05:00
fff7f5ad8e Turn on CORS headers 2023-05-05 08:43:41 -05:00
854c8bf1ba Change to use HttpEntityEnclosingRequestBase instad of Post 2023-05-04 15:43:41 -05:00
a0e45b983f Hide other-versions dropdown while page is loading 2023-05-04 15:42:31 -05:00
05a0e13a44 Add pipes between builds; remove 'time' 2023-05-04 15:39:57 -05:00
8d55ee2706 Rename scriptApi to qqqScriptUtils, putting that into context; 2023-05-04 15:38:49 -05:00
b9d90cdddb fixed failing test because of class cast exception 2023-05-04 15:03:50 -05:00
0ce989b75c CTLE-421: minor bug fixes from demo 2023-05-04 13:55:31 -05:00
405e6a6e2c Merge pull request #17 from Kingsrook/feature/api-home-page
Feature/api home page
2023-05-04 13:53:59 -05:00
74976061d4 Insert test script so it can be found 2023-05-04 12:10:55 -05:00
eed4cc270f Flow table name down 2023-05-04 12:05:48 -05:00
9c5106d7a8 Fix to skip, not blow up, on unrecognized field names 2023-05-03 16:47:03 -05:00
4aa1aed632 Fixes for expoesd joins 2023-05-03 16:36:46 -05:00
c799645658 CTLE-421: updated to check expiration and to get auth0 access token the correct way 2023-05-03 09:29:20 -05:00
5ed02be23f Update to use static times instead of now() in examples, so there aren't always diffs in specs 2023-05-03 08:26:22 -05:00
0159459db2 more flexible (app-defined) security schemes; 2023-05-02 19:58:19 -05:00
b0dbede6fe Add count, totalRows to child record lists 2023-05-02 15:52:20 -05:00
80ae3e1e0c Merge branch 'feature/CTLE-421-migrate-to-use-api-keys' into integration/sprint-25 2023-05-02 15:16:05 -05:00
d300ec162d CTLE-421: checkpoint commit 2023-05-02 15:15:49 -05:00
7625e593d2 Fix merge conlifct w/ bulk insert; add warnings to single-insert; add tests re: insert warnings 2023-05-02 15:08:06 -05:00
d237f4c1ad Make api log methods public 2023-05-02 14:12:05 -05:00
29f26e2ada temporarily lowering coverage limits 2023-05-02 11:42:03 -05:00
845b03bbca Merge branch 'feature/CTLE-421-migrate-to-use-api-keys' into integration/sprint-25 2023-05-02 11:32:27 -05:00
2ddf1fd5c7 CTLE-421: fixed bug caught by tests 2023-05-02 10:34:48 -05:00
83e628d2d6 CTLE-421: added reveal adornment, warnings on child record insert failures 2023-05-02 10:12:55 -05:00
b1ea33b2a2 Move limit & skip from query input to filter 2023-05-02 07:57:50 -05:00
f290cdeb6d Merge branch 'feature/CTLE-207-query-joins' into integration/sprint-25
# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryInput.java
#	qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
2023-05-02 07:52:15 -05:00
476924b030 Merge branch 'feature/CTLE-422-api-for-scripts' into integration/sprint-25
# Conflicts:
#	qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java
2023-05-01 20:13:22 -05:00
e857e87b18 Merge remote-tracking branch 'origin/feature/CTLE-421-migrate-to-use-api-keys' into integration/sprint-25 2023-05-01 20:00:16 -05:00
1a8ab34fe3 make a new collection if given a null one as input. 2023-05-01 14:17:25 -05:00
5070502bde Search for join meta data through exposed joins. 2023-05-01 14:17:08 -05:00
1e053d67ce Update to not NPE is record is null (rather, return null) 2023-05-01 08:32:02 -05:00
15acaec523 More api name & version with scripts (eg, running test scripts) 2023-04-30 19:56:41 -05:00
9ce45934a8 Try to make happier when scripts & apis don't live together 2023-04-28 16:00:06 -05:00
d35f150202 Scripts and apis and all kinds of stuff 2023-04-28 15:46:36 -05:00
0b7c2db452 Introduce RecordPipeBufferedWrapper, to be used in QueryAction when includingAssociations and writing to a pipe 2023-04-28 14:22:29 -05:00
2832566dbd CTLE-421: fixes for failing tests, removed no longer necessary loop 2023-04-28 13:59:08 -05:00
b0d0de5d49 CTLE-421: implemented fieldLevel is hidden, updated to mask password fields 2023-04-28 13:21:08 -05:00
b7e39d6953 Update to allow includeAssociations when querying into a record pipe. This meant propagating a lot of exceptions... 2023-04-28 12:14:12 -05:00
6f99111c52 Move api name & version into ScriptRevision; make the records that go into record-scripts be api versions of records. 2023-04-28 12:11:56 -05:00
4135607a4c Base class for run-script inputs 2023-04-28 12:07:45 -05:00
acfcc422f9 script to open jacoco report 2023-04-28 10:05:25 -05:00
a40f9afd38 Move insert person methods to TestUtils 2023-04-27 20:20:17 -05:00
1314ee98b8 add more store-jacoco calls 2023-04-27 20:19:59 -05:00
6b6dd546fd Initial checkin 2023-04-27 20:19:44 -05:00
37fa78417f Initial checkin 2023-04-27 20:19:31 -05:00
4003323b88 Working version of ApiScriptUtils. Moved actual api imlpementation out of javalin class, into implemnetation class. 2023-04-27 18:55:40 -05:00
5de42b9390 Merge pull request #16 from Kingsrook/feature/todo-more-test-coverage
Feature/todo more test coverage
2023-04-27 15:21:29 -05:00
0c5e3a8002 Many tests. small refactors to support. 2023-04-27 12:50:54 -05:00
d12bf3decc Make it an option in the module's interface, whether or not to query for all records being updated or deleted first (makes more sense for an api backend to NOT do this). 2023-04-27 12:50:46 -05:00
f3509ae1bf Fixed import 2023-04-27 11:12:38 -05:00
dab6a75340 Add dropdown to change apis (and an un-used, but POC method to list apis on a page...) 2023-04-27 11:05:02 -05:00
be09412755 CTLE-421: checkpoint commit of api key story so that CTL test coverage can be worked on instead 2023-04-27 10:31:13 -05:00
e0e4519708 Downgrade some logs 2023-04-26 10:21:22 -05:00
8094c29ec7 handle ExposedJoins in exports 2023-04-26 10:19:12 -05:00
f7f001d430 Improve tense of okSummary 2023-04-26 10:18:55 -05:00
04a8fa94f9 Move skip & limit out of QueryInput, into QQueryFilter... 2023-04-26 10:18:44 -05:00
b4328040aa CTLE-419: updated SyncProcessConfig record to take in performInserts/performUpdates as params 2023-04-25 09:34:21 -05:00
3370da109c CTLE-419: added ability to tell table sync step to either not do inserts or updates, another instant parse format, typo 2023-04-24 18:53:29 -05:00
caf9f102f6 Moved stuff so jacoco reporting happens before failures, i think. Moved untested class reporting into pom, out of circleci 2023-04-24 12:54:42 -05:00
de77f902ac Removed wip code, not meant to be commited 2023-04-24 12:53:39 -05:00
475deee993 Upgrade javalin to 5.4.2 2023-04-24 12:12:54 -05:00
1407f3c63c Add count distinct option to count action 2023-04-24 12:12:41 -05:00
2495989584 add exposed joins to frontend metadata; checkpoing on validation & enrichment of eposed joins 2023-04-24 12:11:46 -05:00
d086284de7 Checkpoint 2023-04-20 16:33:46 -05:00
6ce5845ec8 Checkpoint - good version of getJoinConnections now i think 2023-04-20 16:33:46 -05:00
3bf18e8b51 Initial checkin 2023-04-20 16:33:46 -05:00
88e47ef9ca WIP on getting joins to frontend 2023-04-20 16:33:46 -05:00
912b3885f5 updated nf scripts name 2023-04-20 16:12:22 -05:00
8cc16d44e5 more updates for change to coldtrack 2023-04-20 14:22:26 -05:00
2d81f24887 updated copyrights 2023-04-20 12:27:08 -05:00
3512cde424 Updating to 0.14.0 2023-04-20 10:49:56 -05:00
95dad80d6f Merge tag 'version-0.13.0' into dev
Tag release
2023-04-20 10:49:51 -05:00
72a9f68de7 Merge branch 'release/0.13.0' 2023-04-20 10:48:45 -05:00
b6db572a28 Update for next development version 2023-04-20 10:47:10 -05:00
c9fd7c087a Update versions for release 2023-04-20 10:47:06 -05:00
19d6739723 Merge pull request #15 from Kingsrook/dependabot/maven/qqq-backend-core/org.json-json-20230227
Bump json from 20220320 to 20230227 in /qqq-backend-core
2023-04-20 10:40:58 -05:00
cc881e3168 CTLE-397: fixed overflow issues with manual chips 2023-04-19 15:36:13 -05:00
2f0ef03eec CTLE-397: added pending icon to utils for widget velocity templates 2023-04-19 15:31:20 -05:00
4a65efa5ac CTLE-397: added ability to have footer HTML on widgets 2023-04-19 12:14:15 -05:00
6ebfbc6984 Merge branch 'feature/CTLE-397-oms' of github.com:Kingsrook/qqq into feature/CTLE-397-oms 2023-04-18 22:49:54 -05:00
1f931ddec6 CTLE-397: added more html util methods for chips and process filter links 2023-04-18 22:49:44 -05:00
fc6a72eb09 Replace random ChatGPT method with random StackOverflow method for getting classes from jar 2023-04-18 13:56:08 -05:00
22d0c5c79b (try at least) to set maxLength on string fields 2023-04-18 13:41:26 -05:00
07e575a7d7 Initialize auditInputList as new list, not null 2023-04-18 13:41:10 -05:00
a6e02ea1bf Set url field size XLARGE; set record label field as id 2023-04-18 13:40:53 -05:00
bf5273fa4d Add getAssociationNamesToInclude() 2023-04-18 13:40:32 -05:00
6bc160a213 Merge branch 'feature/CTLE-397-oms' of github.com:Kingsrook/qqq into feature/CTLE-397-oms 2023-04-17 10:45:08 -05:00
a159bc4076 CTLE-397: made some methods static, overloaded ahref filter method 2023-04-17 10:44:55 -05:00
514b70eb88 Add outbound api logs. FTW. 2023-04-14 16:29:07 -05:00
c1c8528dcb Bump json from 20220320 to 20230227 in /qqq-backend-core
Bumps [json](https://github.com/douglascrockford/JSON-java) from 20220320 to 20230227.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-14 18:12:56 +00:00
d760831431 Initial checkin 2023-04-14 09:52:57 -05:00
726fad0e5e Make unique key helper more helpful 2023-04-13 15:34:21 -05:00
d4181cc157 Make unique key helper more helpful 2023-04-13 15:21:43 -05:00
ca9834204b Split fieldNameToLabel out into a method 2023-04-13 11:50:47 -05:00
1b4c754314 Add with'ers 2023-04-13 11:50:31 -05:00
905c2d1296 Move DoFullValidation from validate step to preview step (so you can set from the top-level thing) 2023-04-12 11:25:41 -05:00
cbf84fd76b Fix spelling of Verson; add withers 2023-04-12 11:20:46 -05:00
106976a060 Add option to omitDmlAudits 2023-04-12 11:20:26 -05:00
8f6e4144d2 Change to use getTableWrapperObjectName in places it was intended (e.g., within JSON objects), instead of getTablePath(meant for URLs) 2023-04-12 11:20:12 -05:00
d009169770 Initial add of MultiLevelMapHelper 2023-04-12 11:19:22 -05:00
5453e2e081 Initial add of MetaDataProducer 2023-04-12 11:18:32 -05:00
51b2a9d924 Remove mocked up openapi spec files 2023-04-06 13:17:17 -05:00
a408e4c0d2 Initial checkin 2023-04-06 12:40:55 -05:00
8af83f9286 Include audit label as part of audit 2023-04-06 12:39:38 -05:00
b5c5fb7dd8 Add ability to disable operations per-table, etc. check for insert & update (single) errors better; 2023-04-06 12:39:25 -05:00
12a92c6330 Recursively add all association component schemas; add recordCount to bulk api action logs 2023-04-05 10:03:10 -05:00
0268dad395 Checkstyle 2023-04-04 15:50:43 -05:00
74cd0e0a57 add /apis.json and (re)add .../versions.json paths 2023-04-04 15:45:51 -05:00
e779c392bb Support multiple api's within a q instance. For science! 2023-04-04 13:40:32 -05:00
e35761e0a6 configurable servers; paths in swagger from root 2023-04-04 08:22:30 -05:00
cb5f3ba188 Updated to propagate transactions 2023-04-03 12:00:52 -05:00
b3b7c0b381 Fixed ids on test data. no actual change apparently 2023-04-03 10:46:36 -05:00
f4b8f5c782 Possible values for method, statusCode, apiVersion 2023-04-03 10:46:22 -05:00
ffe8da448b Add api logs user, security fields 2023-04-03 10:35:11 -05:00
b021aebabb Fix NPE with null field list 2023-03-31 16:37:35 -05:00
f79bf85c14 misc fixes 2023-03-31 16:30:56 -05:00
6fbfbe9db2 API Logs! 2023-03-31 16:29:30 -05:00
084630918f Add check for records pre-delete action (for security and better errors); 404s and ids in 207s for bulk update & delete; ignore non-editable fields; 2023-03-31 12:15:17 -05:00
21e3cdd0a5 Making log warn/error messages not have unique values, but instead to use logPairs (for easier grouping in loggly) 2023-03-31 08:25:34 -05:00
5babdd11b6 fixing returned records from memory store to be clones 2023-03-30 19:24:13 -05:00
7e368c6ff9 Adding associated records to Get, Query. 2023-03-30 16:56:18 -05:00
3df4513cd1 Add security, required fields, record-exists validation to UpdateAction. refactor InsertAction to help there 2023-03-30 15:24:40 -05:00
adcddff440 Add 429 (Too Many Requests) error, as option 2023-03-30 11:56:29 -05:00
a9e793dfb8 Check for required fields 2023-03-30 11:55:36 -05:00
f6f2cb7070 Fix test (good catch - 200 for GET /api/, and 404 for bad version doc pages) 2023-03-30 11:53:11 -05:00
944a93bd99 small api doc cleanup; large refactor (resources as resources, not java strings) 2023-03-30 11:31:04 -05:00
7e6a09fc21 Handle null table labels (probably only happens in test, but 🤷) 2023-03-30 08:30:59 -05:00
f15f63b021 oh checkstyle :) 2023-03-29 19:39:11 -05:00
037d67dd38 Add lock NullValueBehavior ALLOW_WRITE_ONLY... fixes a case where audit couldn't be inserted 2023-03-29 18:11:02 -05:00
b0d0c0ce6c much more flesh in openapi, doc page 2023-03-29 18:09:45 -05:00
4a87856601 look for user (client) name in another place 2023-03-29 18:08:36 -05:00
07aa9cfa27 Turning off some capabilities 2023-03-29 18:08:14 -05:00
ef6ccc61c3 Checking record security locks that are more than 1 join away. 2023-03-29 09:55:11 -05:00
e62d2332ac CTLE-307: updated to sort api fields when building spec 2023-03-28 16:53:08 -05:00
0eff8d7d03 Adding requiredField, security validation to insert action 2023-03-28 10:23:53 -05:00
a64a2801c0 CTLE-307: added handling for translating 'too big' auth0 access_tokens into a smaller uuid when authorizing 2023-03-27 21:24:30 -05:00
a43660a05a Manage associations in UpdateAction 2023-03-27 15:07:23 -05:00
df259b5f82 basic validation on Associations 2023-03-27 15:06:58 -05:00
7034671070 Revert -T4 2023-03-27 14:59:42 -05:00
6629015fbf Pin localstack to versiosn 1.4, which seems to NOT be borken. 2023-03-27 14:42:52 -05:00
e9c66b48f2 temp turn of -T4 on mvn verify 2023-03-27 14:07:18 -05:00
ba805a4c92 Initial support for associated records (implemented insert, delete).
Include "api" on audit.
2023-03-27 09:52:39 -05:00
17d4c81cc3 Add LIKE criteria operator; more api endpoints to understand versions, get swagger json; more field name mapping 2023-03-24 10:20:26 -05:00
74cf24a00e Bulk update & delete; errors if more than jsut the expected json 2023-03-23 12:44:40 -05:00
1d2acc7364 Checkstyle!! 2023-03-23 09:21:45 -05:00
11977624bf Add table override name, isExcluded;
Remove more hidden, excluded, etc tables;
Update to comply with rules of openapi spec;
2023-03-23 09:14:56 -05:00
c369430261 implemented replacedBy on removed field; apiFieldName; exclude 2023-03-22 18:38:14 -05:00
94d32fa854 pass tests; redo removed api fields 2023-03-22 15:21:15 -05:00
4da3cc9206 Checkpoint; bulkInsert working; some api instance validation 2023-03-22 15:12:12 -05:00
8ea571ccf7 Remove unused ApiPathNotFoundException 2023-03-22 09:39:48 -05:00
924a6ba31f Checkpoint; working insert, update, delete 2023-03-22 09:33:04 -05:00
3f7f2b58e2 Checkpoint - writing somewhat valid versions of all single-record actions 2023-03-21 17:00:47 -05:00
fd167a7c64 Reverted to copy array, fixes test 2023-03-21 15:17:11 -05:00
f311c7af88 Build out all query operators 2023-03-21 15:02:07 -05:00
af425bd567 exclude model classes from coverge checkstyle-idea.xml 2023-03-21 13:28:53 -05:00
c6fa22524c Coverage on query 2023-03-21 11:50:54 -05:00
303cd4aec0 Passing tests? 2023-03-21 11:13:12 -05:00
90a7745246 cehckpoint - adding security to openapi spec 2023-03-21 11:00:10 -05:00
4a29898405 Working tests 2023-03-21 08:54:16 -05:00
4485898d0e Checkstyle 2023-03-21 08:06:04 -05:00
8924343490 Checkpoint, semi-working query endpoint 2023-03-21 07:59:15 -05:00
f13ee0d1ca Refactoring to work with new API middleware routes 2023-03-20 14:35:31 -05:00
d0e8bd9db2 Initial checkin of qqq-middleware-api 2023-03-20 14:34:39 -05:00
d6a9c8f0e0 Add instance, table, and field-level middleware meta-data 2023-03-20 14:33:27 -05:00
8cfa2736da Add jackson-dataformat-yaml and Initial checkin 2023-03-20 14:33:06 -05:00
9825893f9b Initial checkin 2023-03-20 10:58:46 -05:00
e04d50674b updated to return exception, now that it's in output, not thrown 2023-03-16 16:08:07 -05:00
c9e86896e5 Merge pull request #13 from Kingsrook/feature/column-stats
Feature/column stats
2023-03-16 12:07:31 -05:00
b16eaca394 Let caller specify type to use for an aggregate expression 2023-03-16 11:38:26 -05:00
939dcc308c Fix test; add comment 2023-03-16 09:06:09 -05:00
bf44f97630 Renamed tableStats to columnStats 2023-03-16 07:53:58 -05:00
9391284479 Adjust field labels 2023-03-16 07:53:58 -05:00
ad2eff0e73 More stats & aggregates 2023-03-16 07:53:58 -05:00
bbde64b02d Add table permission check; add display & possible values; 2023-03-16 07:53:58 -05:00
001ec3a34a Add overload that works on list of fields rather than table 2023-03-16 07:53:58 -05:00
dd28c95fc0 Use sessino from context, not input 2023-03-16 07:53:58 -05:00
da17145f66 WIP version of table/column stats process & supporting aggregate changes 2023-03-16 07:53:58 -05:00
03e9f27866 Remove Auth0PermissionsHelper (move to CTL CLI, where it is used) 2023-03-15 18:07:14 -05:00
248db43c8f different exception propagation. 2023-03-15 17:55:53 -05:00
9cbb899788 Add getUsers, getRoleName, getUserName, and getPermissionsForUser 2023-03-15 17:03:09 -05:00
d569541b77 Add some with'ers 2023-03-15 17:03:09 -05:00
21500b642f Add script maxBatchSize, to influence pipe capacity to avoid pipe-full-too-long errors; add link to script logs after running process; add logs to script view screen 2023-03-15 17:03:09 -05:00
fb6cef66ef 2 bugs: clone the filter, and ignore empty-string in condition 2023-03-15 17:03:09 -05:00
4d7c7f48be Add maxRows field (todo - show in UI if you didn't fetch all?) 2023-03-15 17:03:09 -05:00
0e01372200 Make pipeLoop minRecords a parameter; add input to getOverrideRecordPipeCapacity 2023-03-15 17:03:09 -05:00
b6e089a364 Initial checkin 2023-03-15 17:03:09 -05:00
61286cf013 added test to confirm cache use case exclusions support OR boolean operation 2023-03-15 16:21:44 -05:00
7150886e39 Merge branch 'feature/handle-api-request-failures' into dev 2023-03-15 12:04:22 -05:00
7ca9ecbcec Fix to clone a possibleValueSource filter before calling interpret values... added warning to the javadoc on that method - how to make better? 2023-03-15 11:49:40 -05:00
1429d1000c fixed typo, updated validate response to take a list of QRecord objects 2023-03-15 10:50:23 -05:00
54d3e4a6c8 Better/more timezone support 2023-03-14 16:32:34 -05:00
b395ee6778 Initial checkin 2023-03-14 16:32:34 -05:00
ffc574e83f another attempt at fixing binding instants 2023-03-13 20:39:23 -05:00
e0f5c3ff49 added ability to use filters to stop certain records from being cached, fixed insert bug due to binding instants to its default timestamp 2023-03-13 20:02:37 -05:00
ec05f7ab7e Switch to bind Instants as strings instead of timestamps - seems to fix some timezone issues. 2023-03-13 10:41:49 -05:00
33f4c1235a Fixed NPE for null instant 2023-03-13 08:37:35 -05:00
ad8ed4c574 Fix division by zero (check bigDecimal.compareto, not equals 0) 2023-03-09 18:19:02 -06:00
bfe138c018 Fix checkstyle 2023-03-09 16:44:03 -06:00
054c34918d add testScriptProcess 2023-03-09 16:39:14 -06:00
7956c8f455 Added new files missed in last commit 2023-03-09 11:41:03 -06:00
3a172b3fb4 Make postRun methods take a subclass of backendInput/Output, that make it clear they don't have the full record list 2023-03-09 11:36:21 -06:00
e53a982d12 Merge branch 'CTLE-346-add-client-warehouse-int' into dev 2023-03-09 10:01:57 -06:00
02c51ae9ab fixed tostring 2023-03-09 09:43:14 -06:00
eae164c686 Add toStrings 2023-03-09 09:39:15 -06:00
10afa1a80e update to not just assume object is a JSONArray, but to check it to try to avoid some type errors 2023-03-08 16:34:31 -06:00
2164c2115d CTLE-346: fixed select count clause syntax error 2023-03-08 15:56:54 -06:00
00baf64587 Merge pull request #12 from Kingsrook/CTLE-346-add-client-warehouse-int
added ability to log sql to system out, added handling for when joins…
2023-03-08 14:17:37 -06:00
5368903723 CTLE-346: updates from feedback 2023-03-08 14:17:01 -06:00
81a8f100b9 Merge pull request #11 from Kingsrook/feature/record-scripts
Feature/record scripts
2023-03-08 12:39:27 -06:00
7e87950ef5 Fixing (?) placement of when's for circleci 2023-03-08 12:31:56 -06:00
d7abab2fd1 added ability to log sql to system out, added handling for when joins happen and the key field is on the many side 2023-03-08 12:31:36 -06:00
55fa105797 Add when: always to all test reporting steps (store jacocos and find un-tested) 2023-03-08 11:23:48 -06:00
259329e9aa Try to fix find un-tested classes w/ pipefail 2023-03-08 11:13:15 -06:00
46baceee31 More robust test (not based on exact number of tables) 2023-03-08 10:55:08 -06:00
8131dcc644 Updated messages thrown by some non-findable joins 2023-03-08 10:48:09 -06:00
f4ea645055 Fix find un-tested classes; remove slack 2023-03-08 10:24:06 -06:00
f454e0aefa add pvs filters (via post) to table endpoint; more test coverage, plus maybe report on untested classes in ci 2023-03-08 10:18:42 -06:00
11a16590ef Possible value source filtering 2023-03-08 08:39:09 -06:00
5ca3c088a6 Fixed test 2023-03-07 16:59:33 -06:00
1212b47926 Getting test coverage above bar 2023-03-07 16:54:47 -06:00
9526c0b59f Updating to pass tests 2023-03-07 15:51:11 -06:00
1ebe43fe6f Support for run scripts process on table query screen 2023-03-07 15:18:39 -06:00
2162a6832b Removed zombie code 2023-03-07 15:18:17 -06:00
d0fb8c33e9 Update to a workable MVP version of running scripts as table automations 2023-03-07 15:18:08 -06:00
eafa82eb85 initial checkin 2023-03-07 15:14:15 -06:00
47d2291d96 Better handling of joins (flip the join-on if needed) 2023-03-07 10:24:51 -06:00
68686c0e17 Checkpoint 2023-03-07 10:24:51 -06:00
22644b6a36 Initial WIP on record scripts 2023-03-07 10:24:51 -06:00
c640561f53 Initial checkin 2023-03-07 10:24:51 -06:00
c091440848 Add overload of TableSyncProcess.Builder.withExtractStepClass 2023-03-07 10:15:41 -06:00
0a2d1d66da Don't reset qqq-frontend-material-dashboard to SNAPSHOT 2023-03-06 09:51:15 -06:00
60056caa2b Merge branch 'feature/sprint-21' into dev 2023-03-02 15:17:27 -06:00
0c9d6ba912 Attempt at more correct timezone logic in getInstant 2023-03-02 14:54:45 -06:00
17e9a91c86 Add queue-sizes and ad-hock widget value sources; fix some npe's in widget calculation 2023-03-02 14:51:09 -06:00
f0fd0d3fe6 removed the whole omit reload thing which only half worked 2023-03-01 20:08:38 -06:00
d41e92d213 Add new pluralFormat method... 2023-02-27 11:01:29 -06:00
4f3c03de1a Let StreamedETLWithFrontendProcesses have different transaction levels 2023-02-27 10:27:11 -06:00
ea731bac5c Fix test for step without name (enricher now handles) 2023-02-24 17:04:40 -06:00
8a8f0d6e6f avoid run-away PVS caching - clear if over 50,000 entries. 2023-02-24 16:23:08 -06:00
1baf7d8f86 Give good error message if running in a QQQ instance without script tables 2023-02-24 16:18:05 -06:00
4db174b66d Make debug-logging SQL controlled by system property 2023-02-24 16:15:56 -06:00
5074ed1867 infer backend process step name from code name, if present 2023-02-24 15:45:03 -06:00
9be0eb9e76 push & pop action (to get process name in audit) 2023-02-24 15:44:10 -06:00
94a970b8e8 Add warningIcon 2023-02-24 12:42:24 -06:00
80eee299d7 Update to call updateStatusOnlyUpwards 2023-02-23 18:53:01 -06:00
b5aa8e8152 Adding QJavalin Process Handler Test for possibleValues fields in process. 2023-02-23 14:00:19 -06:00
ea795ed701 Missed things re: custom pvs 2023-02-23 13:19:13 -06:00
8440536d35 SPRINT-21: added min height 2023-02-23 11:10:34 -06:00
7ea1750800 make processes able to render a no-code widget output! also search on custom PVS's 2023-02-22 17:50:06 -06:00
4cf8e37e7e Add good getFirstNonNull method 2023-02-22 11:22:14 -06:00
1bdce12b8d Renamed several references from nf to ct 2023-02-21 22:30:49 -06:00
f28af62c5e Update insert action to do pre-step - e.g., to prime amazon s3 client 2023-02-21 22:30:49 -06:00
63be3f01a7 Update to not audit automation status changes 2023-02-21 22:19:20 -06:00
f3cf327384 avoid null pointer on empty record list 2023-02-21 22:18:34 -06:00
d5cb752132 Remove auditInput from the process values (so it doesn't get serialized to frontend) 2023-02-21 14:45:24 -06:00
8833563d26 Update to fetch full records for audit purposes, if security key is needed 2023-02-20 09:58:36 -06:00
5a6a0e2ac5 Add method to let some lower-level actions try to generically update counts, but not to go lower than original counts were. 2023-02-20 09:44:08 -06:00
395f18f513 Fix some process frontend serialization issues 2023-02-20 09:43:26 -06:00
67244d6c6e Initial version of abstract merge duplicates process 2023-02-20 09:43:04 -06:00
05a7f9d847 Audit cleanups (process names for automations; no audit if no fields changed; 2023-02-17 10:25:14 -06:00
049c60c6e3 Remove cp to src/main/ui for nf-one 2023-02-17 10:11:28 -06:00
9659e5b9ea updated snapshot version, fixed parsing of pom.xml when determining version number 2023-02-16 13:15:03 -06:00
0e4fef561e Updating to <revision>0.13.0</revision> 2023-02-16 12:53:21 -06:00
ac074b8492 Merge tag 'version-0.12.0' into dev
Tag release
2023-02-16 12:53:17 -06:00
078536a020 Merge branch 'release/0.12.0' 2023-02-16 12:52:20 -06:00
97b22774f1 Update for next development version 2023-02-16 12:51:07 -06:00
194833bf07 Update versions for release 2023-02-16 12:51:05 -06:00
8924657fc1 automatic audits 2023-02-15 16:16:05 -06:00
3071c63857 turning off evaluateDateTimeParamValues 2023-02-14 11:16:25 -06:00
c07237b7d2 Bump 2023-02-14 10:38:30 -06:00
f01a1ac7a1 2nd iteration on no-code dashboards. add conditional filters, timeframes, more utils, calcualtions 2023-02-14 09:00:50 -06:00
7e07fd04a1 SPRINT-20: made pagination options avaialble for table widgets, updated 'primary color' to come from branding metadata 2023-02-13 13:20:31 -06:00
ff6c2b7fa6 First version of no-code dashboard widgets 2023-02-13 10:43:39 -06:00
d9a17ac99b SPRINT-20: fixed more broken tests due to gettablepath sig change 2023-02-08 22:52:11 -06:00
81f9f4e49a SPRINT-20: updated getTablePath to no longer require input param, added some permission checks to widget links, added utils to get zoned starts of day, year, month 2023-02-08 22:46:49 -06:00
927e7f725a initial checkin 2023-02-08 18:12:47 -06:00
07e6c7019d Add filterExpressions as a concept 2023-02-08 18:12:47 -06:00
eae01bb8c4 update to not make a possible-value field be a record-link if it has a chip too. 2023-02-08 18:12:47 -06:00
2d09a521cd updated to do bulk audits better, along with audit details 2023-02-08 18:12:47 -06:00
92f6f7da04 Add auditInputs to process step outputs, and execution of them in streamed ETL Execute 2023-02-08 18:12:47 -06:00
c863027629 Add okToDelete and error lines 2023-02-08 18:12:47 -06:00
b0cca3f1d7 Add ability to disable one-off lookups 2023-02-08 18:12:47 -06:00
e3c4a3d91d add more date/time format methods 2023-02-08 18:12:47 -06:00
0dcc5ef8c5 Add more overloads (message, throwable, ...logPairs) 2023-02-08 18:12:47 -06:00
f1515e2ba4 Update to accept queryJoins in count and query actions 2023-02-08 18:12:47 -06:00
c620917606 Add print stack trace and exception message 2023-02-08 16:58:21 -06:00
700802f329 ((fixed) bad) Test for HttpDeleteWithBody 2023-02-08 15:08:10 -06:00
f6220482cd (bad) Test for HttpDeleteWithBody 2023-02-08 14:56:46 -06:00
1e1ecbccee Run a little code 2023-02-08 14:36:28 -06:00
1863c31907 Initial checkin 2023-02-08 14:33:00 -06:00
95040d06b8 Initial checkin 2023-02-08 14:29:31 -06:00
dd78c7e51f Add appName and companyUrl 2023-02-03 19:23:20 -06:00
84e2a7e718 SPRINT-20: added setting userId security key value upon qInstance instantiation 2023-02-01 21:55:53 -06:00
668bf5e622 Fix the case where the same source record (identified by sourceKey) is passed in the input more than once (fixes some cases of duplicates) 2023-01-31 13:37:16 -06:00
27b48b62f9 Move check forˆ 404 for unknown report name 2023-01-31 13:36:03 -06:00
583d716f94 Add tense-independent singular/plural methods 2023-01-31 13:34:22 -06:00
142bd70212 Revert "Turning up logging"
This reverts commit e75b645639.
2023-01-30 16:53:51 -06:00
13a7281d3a Switch from using the frustratingly ummutable Collections.emptyList to new ArrayList in JoinsContext for delete-by-filter 2023-01-30 16:51:35 -06:00
f61a3a13be fixed snapshot version 2023-01-30 13:59:24 -06:00
3e56bb4ecd Merge branch 'dev' of github.com:Kingsrook/qqq into dev 2023-01-30 13:40:23 -06:00
789a1c8e63 Updating to <revision>0.12.0</revision> 2023-01-30 13:39:10 -06:00
81d8b66c0c Merge tag 'version-0.11.0' into dev
Tag release
2023-01-30 13:39:05 -06:00
a7faf52817 Merge branch 'release/0.11.0' 2023-01-30 13:37:55 -06:00
45c703a6df Update for next development version 2023-01-30 13:31:20 -06:00
5a9a0754a6 Update versions for release 2023-01-30 13:31:17 -06:00
4790f55243 Fixing scheduled process context; better thread names; add serverInfo endpoint 2023-01-26 21:55:24 -06:00
f0450ef621 PRDONE-170 - Adding support for passing in custom ScriptUtls for scripts. 2023-01-26 12:45:07 -06:00
ed5839aa0a Add /run endpoint for running processes w/o any frontend 2023-01-26 11:23:41 -06:00
56f05c74fc Add secrets manager; update org.json 2023-01-26 11:15:47 -06:00
4f8f8bad9a Initial checkin 2023-01-26 11:15:02 -06:00
4a1853faa5 SPRINT-19: added more chart data and widget meta data 2023-01-25 19:13:35 -06:00
c972f9cecc Update to allow _qStepTimeoutMillis to come from formBody 2023-01-25 10:09:45 -06:00
efa796bb39 Add property/env to add processTag to all logs 2023-01-24 15:56:50 -06:00
a05e74a7d0 SPRINT-19: added stacked bar chart, more widget metadata 2023-01-24 15:52:15 -06:00
41fcb09c70 Update to reset memory record store before and after each 2023-01-24 14:22:09 -06:00
6b12390f6c make scheduler look at env var in addition to system property for deciding whether to start or not. 2023-01-24 14:19:55 -06:00
9889320fa6 add call to validate api responses to doInsert (POST) 2023-01-24 14:08:45 -06:00
c00120a1fc add overloads that take 'String message, LogPair... logPairs' 2023-01-24 13:52:44 -06:00
029f436071 Update to cache script and revision ids (could be better); new AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger, to just do 2 inserts across multiple script runs; add child-log-lines to script log table 2023-01-24 11:57:50 -06:00
f1ff53059f Handle null case in convertInputIdsToEnumIdType 2023-01-24 11:08:44 -06:00
18a8656ba2 Fix enum looks by ids of wrong type 2023-01-24 11:04:34 -06:00
e75b645639 Turning up logging 2023-01-20 15:14:10 -06:00
804efbd608 SPRINT-19: fixed xbar images 2023-01-20 13:13:29 -06:00
fe30d2654b update to implicity add a queryJoin, if a filter field calls out a table name that matches a join in the instance 2023-01-20 10:05:44 -06:00
97b803a86a add audit joins, move names to constants; fixes 2023-01-20 10:03:52 -06:00
2daa42aeb1 Merge branch 'feature/sprint-19' of github.com:Kingsrook/qqq into feature/sprint-19 2023-01-19 16:32:58 -06:00
4344fa472a Initial checkin 2023-01-19 16:31:18 -06:00
6bb810c1f4 Good exception for bad table name 2023-01-19 16:30:59 -06:00
d65af53090 Add some withers 2023-01-19 16:30:44 -06:00
ac6a7ba15a fix setValueIfTableHasField - should catch if field isn't found (helps tables w/o createDate work) 2023-01-19 16:12:30 -06:00
1c150e207a add nonNullArray and mergeLists 2023-01-19 16:12:02 -06:00
f9408716ac update to propagate default values into state before frontend steps 2023-01-19 16:11:45 -06:00
396f02265b explicit errors if context isn't setup 2023-01-19 16:11:19 -06:00
a1674792c6 Merge branch 'dev' into feature/sprint-19 2023-01-18 17:36:46 -06:00
22565b3ecd Initial checkin 2023-01-18 14:19:12 -06:00
d2e7b794f4 moving QLogger package 2023-01-18 12:11:40 -06:00
7000da409a moving standard lambdas to common package 2023-01-18 12:11:40 -06:00
3f0e09e32a Adding javalin access logs 2023-01-18 12:11:40 -06:00
70d9d259c1 Adding heavy field concept 2023-01-18 12:11:35 -06:00
4dc9c52ee0 Adding toLogPair method 2023-01-18 11:56:10 -06:00
964405d210 Initial checkin 2023-01-18 11:55:17 -06:00
369ba3c8d7 switch syslog to a json format (via patternLayout) 2023-01-18 11:46:12 -06:00
24d5406ee3 add filterStackTrace; move unsafeSupplier 2023-01-18 11:40:39 -06:00
984012f3a3 heavy fields, qvalueFormatter static 2023-01-18 11:39:47 -06:00
e8264d915f hotfix: updated to move pie chart label to be specified in the metacritic, added 'add' to pom resolver 2023-01-18 11:33:10 -06:00
46adfa8e24 WIP - logger migrations; initial work for data bag view widget 2023-01-17 10:44:45 -06:00
178078282c Switch to use QLogger everywhere 2023-01-17 10:44:45 -06:00
d03e947889 Merge branch 'dev' into feature/sprint-19 2023-01-17 10:29:16 -06:00
43de1cf749 Add post to download endpoint 2023-01-16 09:03:13 -06:00
d3fa1df56f Implementation of QContext everywhere, instead of passing QInstance and QSession in all ActionInputs 2023-01-15 19:41:23 -06:00
69a6104393 updated snapshot version, improved end of sprint script 2023-01-13 15:59:01 -06:00
e831c75e1e Merge branch 'dev' of github.com:Kingsrook/qqq into dev 2023-01-13 15:56:56 -06:00
eba9b0b4af Updating to <revision>0.11.0</revision> 2023-01-13 15:39:36 -06:00
9f82f35bcf Merge tag 'version-0.10.0' into dev
Tag release
2023-01-13 15:39:32 -06:00
ff07490f94 Merge branch 'release/0.10.0' 2023-01-13 15:38:34 -06:00
680696491f Update for next development version 2023-01-13 15:36:06 -06:00
a35e30059c Update versions for release 2023-01-13 15:36:05 -06:00
a37d22b0d0 Update export to work off post; add check for Authorization as a form param 2023-01-13 14:13:21 -06:00
9e02476ee7 Merge branch 'dev' 2023-01-13 11:02:59 -06:00
2b0974f4a5 Remove single-parent concept on app-children; more working version of recordLock from join 2023-01-13 09:42:31 -06:00
9a58c7683b SPRINT-18: updated module list to include slack and remove lambda 2023-01-12 21:07:55 -06:00
a556bf9764 SPRINT-18: added slack middleware module 2023-01-12 20:16:06 -06:00
41877a7055 Update to not setup a join context for non-table data sources 2023-01-12 08:38:09 -06:00
c07a77d4a6 Updated to disable view-all link if tablePath can't be found (e.g., because user can't view table) 2023-01-11 16:44:42 -06:00
23e9abeb74 implementation of record security locks, and permissions 2023-01-11 13:08:59 -06:00
e4d37e3db9 Add variant to pre-load by in-list, to cache misses as null 2023-01-05 14:33:32 -06:00
4b31a8b4bb Merge branch 'refs/heads/0.7.0-12-hotfix' into feature/sprint-18 2023-01-05 14:26:20 -06:00
d7e7315dc8 Merge branch 'feature/auth0-basic-auth-reuse' into feature/sprint-18 2023-01-05 11:36:39 -06:00
de05e4ae58 SPRINT-18: fixed unit test on auth0 reuse 2023-01-04 22:20:38 -06:00
150582964b Change setupSession to be public 2023-01-04 12:05:19 -06:00
2874b98b66 Add re-use of tokens from basicAuth 2023-01-04 10:07:44 -06:00
7fae3e2329 Add table-based authentication module; update javalin to support Authentication: Basic header; Move authentication classes 2022-12-28 17:00:08 -06:00
428f48602b Better testing on join reports, possible value translations; renamed left & right in QueryJoin (now joinTable, baseTable) 2022-12-22 13:46:32 -06:00
799b695e14 Checkpoint on report and export changes, possible value translating 2022-12-21 11:37:16 -06:00
19d88910b5 Updating table sync api 2022-12-20 10:56:59 -06:00
2ad4b22f55 Add withSchedule method 2022-12-19 15:02:20 -06:00
040dae55d5 Add withSchedule; fix return on withBasepullConfiguration; add overload withTransformStepClass 2022-12-19 14:59:58 -06:00
dd9253fde4 Updated comment about field behaviorfs 2022-12-19 14:56:37 -06:00
8a4d5bfb34 Update to not NPE if data source doesn't have a filter 2022-12-19 14:56:25 -06:00
e1c53b9d48 Updated interface in sync processes; more status updates in ETL processes; Basepull only update timestamp if ran as basepull; javalin report endpoint; 2022-12-19 12:04:01 -06:00
a6656af040 Updated for older api on QQueryFilter (noarg constructor) 2022-12-15 16:18:14 -06:00
0e68bf1e72 Hotfix - sqs batch mode, and rdbms delete-by-filter 2022-12-15 16:13:56 -06:00
1b672afcd0 Fixed rdbms delete test 2022-12-15 16:04:46 -06:00
5005c38c18 Fixes for performance of sqs (batch mode), plus bug in deleteAction by query-filter (with test that proves it) 2022-12-15 15:54:58 -06:00
1cdf437551 SPRINT-18: upped current snapshot version 2022-12-15 15:39:33 -06:00
04cddb8d5d Updating to <revision>0.9.0</revision> 2022-12-15 15:34:43 -06:00
0d6bd013f5 Merge branch 'release/0.9.0' 2022-12-15 15:33:49 -06:00
7b6284c06f Update for next development version 2022-12-15 15:24:00 -06:00
edcc27485e Update versions for release 2022-12-15 15:23:59 -06:00
280373ddc5 SPRINT-17: added code coverage test class to meet minimum requirements 2022-12-15 10:36:43 -06:00
25815ebc25 SPRINT-17: updated some widgets to look less broken when data is 'not available now', checkpoint commit on 'real dashboards' 2022-12-15 10:20:48 -06:00
59cbf83860 Add field increaseIsGood 2022-12-14 14:53:03 -06:00
6186b17e92 Add environmentValues to qInstance 2022-12-14 14:50:45 -06:00
30003b729c Add overload of getForeignRecordMap that takes additional (base) filter for query 2022-12-09 20:25:59 -06:00
293b3e4207 Add qruntime exception; let transform step set pipe capacity 2022-12-09 16:44:53 -06:00
470321dcf6 add equals and hashcode 2022-12-09 16:06:26 -06:00
48cfdeffa1 Change post-query customizer to be class (that can do list), not function 2022-12-09 12:05:39 -06:00
14c7fbe370 Moving dropdowns to work for all widgets 2022-12-09 09:53:13 -06:00
8454f94020 SPRINT-17: updated dropdowns to be required, added divider 2022-12-08 15:06:40 -06:00
697261f91b Add aHrefViewRecord 2022-12-08 10:37:11 -06:00
61d493a4f5 Add FieldValueList widget type, more html-helper methods 2022-12-08 09:18:52 -06:00
c21c89e85f SPRINT-17: test was not using groupby to fetch values 2022-12-07 16:43:27 -06:00
a9a3e3b19e SPRINT-17: fixed dumb checkstyle violation 2022-12-07 16:31:04 -06:00
5cf9e7b60d SPRINT-17: fixed failing test and removed pvs name member from parent meta data 2022-12-07 15:47:15 -06:00
9b34ee7fe7 SPRINT-17: updates to parent widget dropdown data, updated group bys to be objects allowing group by with custom formats 2022-12-07 15:31:48 -06:00
241741e2e5 Fix default for canAddChildRecord to be false; add more link-generating functions (to child-modals on view screens, etc) 2022-12-06 15:57:32 -06:00
a769d8942c Adding unique key check to insert action; adding post-insert customizer 2022-12-05 15:46:17 -06:00
c22fc89cbb SPRINT-17: changed some variable names 2022-12-05 12:18:23 -06:00
060da69afb Adding table-cacheOf concept; ability to add a child record from child-list widget 2022-12-05 10:26:53 -06:00
3691ad87e5 Merge remote-tracking branch 'origin/0.7.0-12-hotfix' into dev 2022-12-01 16:59:19 -06:00
583ac70563 Merge tag 'version-0.8.0' into dev
Tag release
2022-12-01 16:56:13 -06:00
2afd8864a8 Merge branch 'release/0.8.0' 2022-12-01 16:53:46 -06:00
28e0bdadbf Update for next development version 2022-12-01 16:49:34 -06:00
c856a7b0b3 updated versions for release 2022-12-01 16:33:37 -06:00
3a721d8df5 cd to correct directory after environment setup 2022-12-01 15:56:52 -06:00
ab08a99477 updated to move to proper directory before beginning work 2022-12-01 15:44:34 -06:00
e170dab1db SPRINT-16: added ERROR adornment type 2022-11-30 16:52:20 -06:00
40afb629e7 Add overrideBatchSize to table automation details 2022-11-30 11:30:18 -06:00
ce2cccfa2a SPRINT-16: added more tests 2022-11-30 11:29:48 -06:00
6813617a21 SPRINT-16: added new widget types, moved some things to a different package, etc. 2022-11-29 14:34:59 -06:00
2df9576e20 Initial checkin 2022-11-28 15:19:14 -06:00
792383d21a Add ConvertHtmlToPdfAction 2022-11-28 11:52:48 -06:00
b2d76e8206 Much implementation of joins for RDBMS 2022-11-23 16:37:54 -06:00
6685e61500 Merge branch 'dev' into feature/sprint-16 2022-11-22 13:01:50 -06:00
aef8f5cd59 Adding maxLength to fields, along with initial version of FieldBehviors and ValueBehaviorApplier, including ValueTooLongBehavior 2022-11-22 12:59:57 -06:00
26dd323f34 SPRINT-16: upped current snapshot version 2022-11-22 12:24:06 -06:00
209ada8065 Add more util methods; add AbstractHTMLWidgetRenderer 2022-11-21 14:46:14 -06:00
105b2c92c9 Add aggregateAction; Add renderTemplateAction 2022-11-18 16:51:54 -06:00
1d1461deea Adding filesystem writing - used by javalin to store uploaded files; done async, via new base class for actions 2022-11-17 19:59:29 -06:00
6b2860e303 Initial checkin 2022-11-17 15:59:13 -06:00
0233edc7b2 Updating to <revision>0.7.0</revision> 2022-11-17 12:13:02 -06:00
f2dbe5bf12 Merge branch 'release/0.7.0' 2022-11-17 12:12:12 -06:00
31c1a8ecb6 Update for next development version 2022-11-17 12:07:27 -06:00
373ff20e9d Update versions for release 2022-11-17 12:07:26 -06:00
3305fb76d5 Fixed to insert & update correct lists 2022-11-16 14:42:22 -06:00
d2d32e5ab1 SPRINT-15: reverted log change 2022-11-16 10:22:16 -06:00
2920774266 SPRINT-15: reverted log change 2022-11-16 10:18:57 -06:00
03bce59f2a SPRINT-15: reverted log change 2022-11-16 10:14:14 -06:00
955a30a660 SPRINT-15: attempt to improve json logging 2022-11-16 08:55:02 -06:00
d2bfe9f610 SPRINT-15: attempt to improve json logging 2022-11-15 21:44:28 -06:00
798dc6e6e5 Merge branch 'feature/sprint-15' of github.com:Kingsrook/qqq into feature/sprint-15 2022-11-15 21:15:23 -06:00
126d51c8f9 SPRINT-15: attempt to improve json logging 2022-11-15 21:14:06 -06:00
7d72487a75 Initial checkin 2022-11-15 15:41:46 -06:00
848a09cef5 SPRINT-15: attempt to fix loggly datetimes 2022-11-15 12:17:32 -06:00
322efa2102 Initial checkin 2022-11-15 10:47:49 -06:00
20607bbf9b SPRINT-15: added initial version of the QLogger class 2022-11-14 17:37:47 -06:00
430e1bc9c7 Add table name to record pipe loop 2022-11-14 14:00:55 -06:00
2a7e76b0f9 Add joins and ChildRecordList widget 2022-11-14 14:00:55 -06:00
f0a464ce9e SPRINT-15: trying to fix circle ci 2022-11-14 12:30:02 -06:00
e305c64572 SPRINT-15: trying to fix circle ci 2022-11-14 12:26:56 -06:00
514f105c86 SPRINT-15: ¯\_(ツ)_/¯ 2022-11-14 12:06:57 -06:00
87dc7fd96c SPRINT-15: adding reinstalling of ca-certificates to try to fix circle ci failures 2022-11-14 11:38:20 -06:00
d54e0c71a8 SPRINT-15: moved response validation into its own method which can be overriden in subclasses 2022-11-14 10:47:02 -06:00
a4ad8ac08a SPRINT-15: added default constructor 2022-11-13 14:35:46 -06:00
b2aaffeb6d SPRINT-15: updates to logging 2022-11-11 21:48:50 -06:00
11aa55cf40 SPRINT-15: refactor of api actions, moving logics into utils class, unified all calls to apis, clean ups, etc. 2022-11-11 21:39:05 -06:00
a2da8c4127 Add withValues(Pair...) and iconAndColorValues 2022-11-11 16:20:50 -06:00
8b31cee890 Add TableSyncProcess 2022-11-11 14:34:13 -06:00
a5ec33b51b Script Tests and further enhancements 2022-11-10 14:22:43 -06:00
6d08afa4c2 SPRINT-15: added json util helper method 2022-11-10 13:21:11 -06:00
6496986aab bump 2022-11-09 11:05:08 -06:00
f9c14eb08c update to use same try-with-resources for CloseableHttpClient and CloseableHttpResponse 2022-11-09 11:02:25 -06:00
230dde2e52 Refactoring javascript executor scripts.main error handling 2022-11-09 10:36:37 -06:00
e701ae0ea3 add try-catch around script.main business 2022-11-09 10:03:14 -06:00
955294ae18 Update javascript executor to work w/ compiled ts scripts that export a main function; add output to javalin storeRecordAssociatedScript 2022-11-09 09:49:58 -06:00
2975b90505 Allowing load step to set pipe sizes to avoid 'Giving up adding record to pipe' in easypost tracker creation; Make status never have current > total; 2022-11-09 08:44:16 -06:00
1e09931218 SPRINT-15: updated apiupdateAction to handle 200/207 better, added better exception handling 2022-11-08 16:33:06 -06:00
8932ef891e ADd method objectToMap 2022-11-08 09:28:37 -06:00
1a287fe35a Log and thread name adjustments - trying to make loggly more useful 2022-11-08 09:08:15 -06:00
236eff523e SPRINT-15: added methods to get lists and maps of record entities 2022-11-04 14:28:15 -05:00
e815bd37ab Merge branch 'dev' into feature/sprint-15 2022-11-04 10:02:33 -05:00
669b6d3cb7 Adding status object in standard loadVia steps and updating it in api insert; add user timezone header to session 2022-11-04 09:59:07 -05:00
f99430d2bc sprint-15:updated current snapshot version 2022-11-03 15:09:28 -05:00
ebf9556a5d Merge branch 'feature/sprint-14' into feature/sprint-15 2022-11-03 14:36:32 -05:00
a3f9df09a9 Updating to 0.7.0 2022-11-03 14:31:42 -05:00
2105e3dfc5 sprint-14: changed update to do a post not put 2022-11-03 14:29:40 -05:00
99561ecf81 Merge tag 'version-0.6.0' into dev
Tag release
2022-11-03 11:57:15 -05:00
bd85526261 Update for next development version 2022-11-03 11:45:59 -05:00
831 changed files with 111661 additions and 5816 deletions

23
.circleci/adjust-pom-version.sh Executable file
View File

@ -0,0 +1,23 @@
#!/bin/bash
if [ -z "$CIRCLE_BRANCH" ] && [ -z "$CIRCLE_TAG" ]; then
echo "Error: env vars CIRCLE_BRANCH and CIRCLE_TAG were not set."
exit 1;
fi
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
if [ -n "$CIRCLE_BRANCH" ]; then
SLUG=$(echo $CIRCLE_BRANCH | sed 's/[^a-zA-Z0-9]/-/g')
else
SLUG=$(echo $CIRCLE_TAG | sed 's/^snapshot-//g')
fi
POM=$(dirname $0)/../pom.xml
echo "Updating $POM <revision> to: $SLUG-SNAPSHOT"
sed -i "s/<revision>.*/<revision>$SLUG-SNAPSHOT<\/revision>/" $POM
git diff $POM

View File

@ -1,7 +1,6 @@
version: 2.1
orbs:
slack: circleci/slack@4.10.1
localstack: localstack/platform@1.0
commands:
@ -12,18 +11,26 @@ commands:
steps:
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/index.html
when: always
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/jacoco-resources
when: always
install_java17:
steps:
- run:
name: Install Java 17
command: |
sudo add-apt-repository -y ppa:openjdk-r/ppa
sudo apt-get update
sudo apt install -y openjdk-17-jdk
sudo rm /etc/alternatives/java
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
- run:
## used by jacoco uncovered class reporting in pom.xml
name: Install html2text
command: |
sudo apt-get update
sudo apt-get install -y html2text
mvn_verify:
steps:
@ -45,10 +52,18 @@ commands:
module: qqq-backend-module-filesystem
- store_jacoco_site:
module: qqq-backend-module-rdbms
- store_jacoco_site:
module: qqq-backend-module-api
- store_jacoco_site:
module: qqq-middleware-api
- store_jacoco_site:
module: qqq-middleware-javalin
- store_jacoco_site:
module: qqq-middleware-picocli
- store_jacoco_site:
module: qqq-middleware-slack
- store_jacoco_site:
module: qqq-language-support-javascript
- store_jacoco_site:
module: qqq-sample-project
- run:
@ -67,6 +82,10 @@ commands:
mvn_jar_deploy:
steps:
- checkout
- run:
name: Adjust pom version
command: |
.circleci/adjust-pom-version.sh
- restore_cache:
keys:
- v1-dependencies-{{ checksum "pom.xml" }}
@ -86,8 +105,6 @@ jobs:
- localstack/startup
- install_java17
- mvn_verify
- slack/notify:
event: fail
mvn_deploy:
executor: localstack/default
@ -96,14 +113,12 @@ jobs:
- install_java17
- mvn_verify
- mvn_jar_deploy
- slack/notify:
event: always
workflows:
test_only:
jobs:
- mvn_test:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
ignore: /dev/
@ -113,7 +128,7 @@ workflows:
deploy:
jobs:
- mvn_deploy:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters:
branches:
only: /dev/

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

View File

@ -262,6 +262,7 @@
<module name="Header">
<property name="headerFile" value="checkstyle/license.txt"/>
<property name="fileExtensions" value="java"/>
<property name="ignoreLines" value="3"/>
</module>
<module name="SuppressWarningsFilter"/>
</module>

107
pom.xml
View File

@ -37,12 +37,14 @@
<module>qqq-middleware-picocli</module>
<module>qqq-middleware-javalin</module>
<module>qqq-middleware-lambda</module>
<module>qqq-middleware-slack</module>
<module>qqq-middleware-api</module>
<module>qqq-utility-lambdas</module>
<module>qqq-sample-project</module>
</modules>
<properties>
<revision>0.6.0</revision>
<revision>0.19.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -204,6 +206,63 @@
<skipUpdateVersion>true</skipUpdateVersion>
</configuration>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<version>3.0.0</version>
<executions>
<execution>
<id>test-coverage-summary</id>
<phase>verify</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>sh</executable>
<arguments>
<argument>-c</argument>
<argument>
<![CDATA[
if [ ! -e target/site/jacoco/index.html ]; then
echo "No jacoco coverage report here.";
exit;
fi
echo
echo "Jacoco coverage summary report:"
echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
else
echo "xpath is not installed. Jacoco coverage summary will not be produced here...";
fi
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Untested classes, per Jacoco:"
echo "-----------------------------"
for i in target/site/jacoco/*/index.html; do
html2text -width 500 -nobs $i | sed '1,/^Total/d;' | grep -v Created | sed 's/ \+/ /g' | sed 's/ [[:digit:]]$//' | grep -v 0$ | cut -d' ' -f1;
done;
echo
else
echo "html2text is not installed. Untested classes from Jacoco will not be printed here...";
fi
]]>
</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@ -247,56 +306,14 @@
</execution>
<execution>
<id>post-unit-test</id>
<phase>verify</phase>
<!-- <phase>verify</phase> -->
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<groupId>org.codehaus.mojo</groupId>
<version>3.0.0</version>
<executions>
<execution>
<id>test-coverage-summary</id>
<phase>verify</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>sh</executable>
<arguments>
<argument>-c</argument>
<argument>
<![CDATA[
if [ ! -e target/site/jacoco/index.html ]; then
echo "No jacoco coverage report here.";
exit;
fi
echo
echo "Jacoco coverage summary report:"
echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------"
which xpath > /dev/null 2>&1
if [ "$?" == "0" ]; then
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values
else
echo "xpath is not installed. Jacoco coverage summary will not be produced here..";
fi
]]>
</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -56,20 +56,35 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>quicksight</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apigateway</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-secretsmanager</artifactId>
<version>1.12.385</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.0-rc1</version>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
<version>20230227</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@ -89,14 +104,41 @@
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>mvc-auth-commons</artifactId>
<version>1.9.2</version>
<artifactId>auth0</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.22.0</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>java-dotenv</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!-- the next 2 deps are for html to pdf - per https://www.baeldung.com/java-html-to-pdf -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.15.3</version>
</dependency>
<dependency>
<groupId>org.xhtmlrenderer</groupId>
<artifactId>flying-saucer-pdf-openpdf</artifactId>
<version>9.1.22</version>
</dependency>
<!-- the next 3 deps are being added for google drive support -->
<dependency>
@ -150,6 +192,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,77 @@
/*
* 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
** Base class for QQQ Actions (both framework and application defined) that
** have a signature like a BiConsumer - taking both Input and Output objects as
** parameters, with void output.
*******************************************************************************/
public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O extends AbstractActionOutput>
{
/*******************************************************************************
**
*******************************************************************************/
public abstract void execute(I input, O output) throws QException;
/*******************************************************************************
**
*******************************************************************************/
public Future<Void> executeAsync(I input, O output)
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<Void> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() ->
{
try
{
QContext.init(capturedContext);
execute(input, output);
completableFuture.complete(null);
}
catch(QException e)
{
completableFuture.completeExceptionally(e);
}
finally
{
QContext.clear();
}
});
return (completableFuture);
}
}

View File

@ -0,0 +1,77 @@
/*
* 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;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
** Base class for QQQ Actions (both framework and application defined) that
** have a signature like a Function - taking an Input object as a parameter,
** and returning an Output object.
*******************************************************************************/
public abstract class AbstractQActionFunction<I extends AbstractActionInput, O extends AbstractActionOutput>
{
/*******************************************************************************
**
*******************************************************************************/
public abstract O execute(I input) throws QException;
/*******************************************************************************
**
*******************************************************************************/
public Future<O> executeAsync(I input)
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<O> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() ->
{
try
{
QContext.init(capturedContext);
O output = execute(input);
completableFuture.complete(output);
}
catch(QException e)
{
completableFuture.completeExceptionally(e);
}
finally
{
QContext.clear();
}
});
return (completableFuture);
}
}

View File

@ -25,9 +25,12 @@ package com.kingsrook.qqq.backend.core.actions;
import java.io.Serializable;
import java.util.List;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
@ -43,9 +46,22 @@ public class ActionHelper
*******************************************************************************/
public static void validateSession(AbstractActionInput request) throws QException
{
QInstance qInstance = QContext.getQInstance();
QSession qSession = QContext.getQSession();
if(qInstance == null)
{
throw (new QException("QInstance was not set in QContext."));
}
if(qSession == null)
{
throw (new QException("QSession was not set in QContext."));
}
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData());
if(!authenticationModule.isSessionValid(request.getInstance(), request.getSession()))
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
if(!authenticationModule.isSessionValid(qInstance, qSession))
{
throw new QAuthenticationException("Invalid session in request");
}

View File

@ -78,9 +78,7 @@ public class AsyncJobCallback
public void updateStatus(String message, int current, int total)
{
this.asyncJobStatus.setMessage(message);
this.asyncJobStatus.setCurrent(current);
this.asyncJobStatus.setTotal(total);
storeUpdatedStatus();
updateStatus(current, total); // this call will storeUpdatedStatus.
}
@ -90,13 +88,65 @@ public class AsyncJobCallback
*******************************************************************************/
public void updateStatus(int current, int total)
{
this.asyncJobStatus.setCurrent(current);
this.asyncJobStatus.setCurrent(current > total ? total : current);
this.asyncJobStatus.setTotal(total);
storeUpdatedStatus();
}
/*******************************************************************************
** Update the current and total fields, but ONLY if the new values are
** both >= the previous values.
*******************************************************************************/
public void updateStatusOnlyUpwards(int current, int total)
{
boolean currentIsOkay = (this.asyncJobStatus.getCurrent() == null || this.asyncJobStatus.getCurrent() <= current);
boolean totalIsOkay = (this.asyncJobStatus.getTotal() == null || this.asyncJobStatus.getTotal() <= total);
if(currentIsOkay && totalIsOkay)
{
updateStatus(current, total);
}
}
/*******************************************************************************
** Increase the 'current' value in the '1 of 2' sense.
*******************************************************************************/
public void incrementCurrent()
{
incrementCurrent(1);
}
/*******************************************************************************
** Increase the 'current' value in the '1 of 2' sense.
*******************************************************************************/
public void incrementCurrent(int amount)
{
if(asyncJobStatus.getCurrent() != null)
{
if(asyncJobStatus.getTotal() != null && asyncJobStatus.getCurrent() + amount > asyncJobStatus.getTotal())
{
/////////////////////////////////////////////////////
// make sure we don't ever make current > total... //
/////////////////////////////////////////////////////
asyncJobStatus.setCurrent(asyncJobStatus.getTotal());
}
else
{
asyncJobStatus.setCurrent(asyncJobStatus.getCurrent() + amount);
}
storeUpdatedStatus();
}
}
/*******************************************************************************
** Remove the values from the current & total fields
*******************************************************************************/

View File

@ -30,13 +30,18 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.Level;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -44,8 +49,9 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class AsyncJobManager
{
private static final Logger LOG = LogManager.getLogger(AsyncJobManager.class);
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
private String forcedJobUUID = null;
/*******************************************************************************
@ -65,15 +71,18 @@ 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);
try
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<T> future = CompletableFuture.supplyAsync(() ->
{
QContext.init(capturedContext);
return (runAsyncJob(jobName, asyncJob, uuidAndTypeStateKey, asyncJobStatus));
});
@ -91,7 +100,7 @@ public class AsyncJobManager
}
catch(TimeoutException e)
{
LOG.info("Job going async " + uuidAndTypeStateKey.getUuid());
LOG.debug("Job going async " + uuidAndTypeStateKey.getUuid());
throw (new JobGoingAsyncException(uuidAndTypeStateKey.getUuid().toString()));
}
}
@ -147,12 +156,17 @@ public class AsyncJobManager
asyncJobStatus.setState(AsyncJobState.ERROR);
asyncJobStatus.setCaughtException(e);
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
LOG.warn("Job " + uuidAndTypeStateKey.getUuid() + " ended with an exception: ", e);
//////////////////////////////////////////////////////
// if user facing, just log an info, warn otherwise //
//////////////////////////////////////////////////////
LOG.log((e instanceof QUserFacingException) ? Level.INFO : Level.WARN, "Job ended with an exception", e, logPair("jobId", uuidAndTypeStateKey.getUuid()));
throw (new CompletionException(e));
}
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}
@ -194,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,11 +25,13 @@ 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;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
/*******************************************************************************
@ -40,13 +42,14 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class AsyncRecordPipeLoop
{
private static final Logger LOG = LogManager.getLogger(AsyncRecordPipeLoop.class);
private static final QLogger LOG = QLogger.getLogger(AsyncRecordPipeLoop.class);
private static final int TIMEOUT_AFTER_NO_RECORDS_MS = 10 * 60 * 1000;
private static final int MAX_SLEEP_MS = 1000;
private static final int INIT_SLEEP_MS = 10;
private static final int MIN_RECORDS_TO_CONSUME = 10;
private static final int MAX_SLEEP_MS = 1000;
private static final int INIT_SLEEP_MS = 10;
private Integer minRecordsToConsume = 10;
@ -62,7 +65,7 @@ public class AsyncRecordPipeLoop
** @param consumer lambda that consumes records from the pipe
* e.g., a transform/load step.
*******************************************************************************/
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable> supplier, UnsafeSupplier<Integer> consumer) throws QException
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable, QException> supplier, UnsafeSupplier<Integer, QException> consumer) throws QException
{
///////////////////////////////////////////////////
// start the extraction function as an async job //
@ -82,7 +85,7 @@ public class AsyncRecordPipeLoop
while(jobState.equals(AsyncJobState.RUNNING))
{
if(recordPipe.countAvailableRecords() < MIN_RECORDS_TO_CONSUME)
if(recordPipe.countAvailableRecords() < minRecordsToConsume)
{
///////////////////////////////////////////////////////////////
// if the pipe is too empty, sleep to let the producer work. //
@ -140,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);
///////////////////////////////////
@ -178,29 +186,32 @@ public class AsyncRecordPipeLoop
/*******************************************************************************
**
** Getter for minRecordsToConsume
*******************************************************************************/
@FunctionalInterface
public interface UnsafeFunction<T, R>
public Integer getMinRecordsToConsume()
{
/*******************************************************************************
**
*******************************************************************************/
R apply(T t) throws QException;
return (this.minRecordsToConsume);
}
/*******************************************************************************
**
** Setter for minRecordsToConsume
*******************************************************************************/
@FunctionalInterface
public interface UnsafeSupplier<T>
public void setMinRecordsToConsume(Integer minRecordsToConsume)
{
/*******************************************************************************
**
*******************************************************************************/
T get() throws QException;
this.minRecordsToConsume = minRecordsToConsume;
}
/*******************************************************************************
** Fluent setter for minRecordsToConsume
*******************************************************************************/
public AsyncRecordPipeLoop withMinRecordsToConsume(Integer minRecordsToConsume)
{
this.minRecordsToConsume = minRecordsToConsume;
return (this);
}
}

View File

@ -0,0 +1,406 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.audits;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.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;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditSingleInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.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;
/*******************************************************************************
** Insert 1 or more audits (and optionally their children, auditDetails)
**
** Takes care of managing the foreign key tables (auditTable, auditUser).
**
** Enforces that security key values are provided, if the table has any. Note that
** might mean a null is given for a particular key, but at least the key must be present.
*******************************************************************************/
public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput>
{
private static final QLogger LOG = QLogger.getLogger(AuditAction.class);
private Map<Pair<String, String>, Integer> cachedFetches = new HashMap<>();
/*******************************************************************************
** Execute to insert 1 audit, with no details (child records)
*******************************************************************************/
public static void execute(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
{
execute(tableName, recordId, securityKeyValues, message, null);
}
/*******************************************************************************
** 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
*******************************************************************************/
public static void execute(String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<QRecord> details)
{
new AuditAction().execute(new AuditInput().withAuditSingleInput(new AuditSingleInput()
.withAuditTableName(tableName)
.withRecordId(recordId)
.withSecurityKeyValues(securityKeyValues)
.withMessage(message)
.withDetails(details)
));
}
/*******************************************************************************
** 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 void appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
{
appendToInput(auditInput, tableName, recordId, securityKeyValues, message, null);
}
/*******************************************************************************
** Add 1 auditSingleInput to an AuditInput object - with a list of details (child records).
*******************************************************************************/
public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message, List<QRecord> details)
{
if(auditInput == null)
{
auditInput = new AuditInput();
}
return auditInput.withAuditSingleInput(new AuditSingleInput()
.withAuditTableName(tableName)
.withRecordId(recordId)
.withSecurityKeyValues(securityKeyValues)
.withMessage(message)
.withDetails(details)
);
}
/*******************************************************************************
** 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;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public AuditOutput execute(AuditInput input)
{
AuditOutput auditOutput = new AuditOutput();
if(CollectionUtils.nullSafeHasContents(input.getAuditSingleInputList()))
{
try
{
List<QRecord> auditRecords = new ArrayList<>();
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
{
/////////////////////////////////////////
// validate table is known in instance //
/////////////////////////////////////////
QTableMetaData table = QContext.getQInstance().getTable(auditSingleInput.getAuditTableName());
if(table == null)
{
throw (new QException("Requested audit for an unrecognized table name: " + auditSingleInput.getAuditTableName()));
}
///////////////////////////////////////////////////
// validate security keys on the table are given //
///////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + auditSingleInput.getAuditTableName()));
}
}
////////////////////////////////////////////////
// map names to ids and handle default values //
////////////////////////////////////////////////
Integer auditTableId = getIdForName("auditTable", auditSingleInput.getAuditTableName());
Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(auditSingleInput.getAuditUserName(), getSessionUserName()));
Instant timestamp = Objects.requireNonNullElse(auditSingleInput.getTimestamp(), Instant.now());
//////////////////
// build record //
//////////////////
QRecord record = new QRecord()
.withValue("auditTableId", auditTableId)
.withValue("auditUserId", auditUserId)
.withValue("timestamp", timestamp)
.withValue("message", auditSingleInput.getMessage())
.withValue("recordId", auditSingleInput.getRecordId());
if(auditSingleInput.getSecurityKeyValues() != null)
{
for(Map.Entry<String, Serializable> entry : auditSingleInput.getSecurityKeyValues().entrySet())
{
record.setValue(entry.getKey(), entry.getValue());
}
}
auditRecords.add(record);
}
/////////////////////////////
// do a single bulk insert //
/////////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit");
insertInput.setRecords(auditRecords);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
//////////////////////////////////////////
// now look for children (auditDetails) //
//////////////////////////////////////////
int i = 0;
List<QRecord> auditDetailRecords = new ArrayList<>();
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
{
Integer auditId = insertOutput.getRecords().get(i++).getValueInteger("id");
if(auditId == null)
{
LOG.warn("Missing an id for inserted audit - so won't be able to store its child details...");
continue;
}
for(QRecord detail : CollectionUtils.nonNullList(auditSingleInput.getDetails()))
{
auditDetailRecords.add(detail.withValue("auditId", auditId));
}
}
if(!auditDetailRecords.isEmpty())
{
insertInput = new InsertInput();
insertInput.setTableName("auditDetail");
insertInput.setRecords(auditDetailRecords);
new InsertAction().execute(insertInput);
}
}
catch(Exception e)
{
LOG.error("Error performing an audit", e);
}
}
return (auditOutput);
}
/*******************************************************************************
**
*******************************************************************************/
private static String getSessionUserName()
{
QUser user = QContext.getQSession().getUser();
if(user == null)
{
return ("Unknown");
}
return (user.getFullName());
}
/*******************************************************************************
**
*******************************************************************************/
private Integer getIdForName(String tableName, String nameValue) throws QException
{
Pair<String, String> key = new Pair<>(tableName, nameValue);
if(!cachedFetches.containsKey(key))
{
Integer id = fetchIdFromName(tableName, nameValue);
if(id != null)
{
cachedFetches.put(key, id);
return id;
}
try
{
LOG.debug("Inserting " + tableName + " named " + nameValue);
InsertInput insertInput = new InsertInput();
insertInput.setTableName(tableName);
QRecord record = new QRecord().withValue("name", nameValue);
if(tableName.equals("auditTable"))
{
QTableMetaData table = QContext.getQInstance().getTable(nameValue);
if(table != null)
{
record.setValue("label", table.getLabel());
}
}
insertInput.setRecords(List.of(record));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
id = insertOutput.getRecords().get(0).getValueInteger("id");
if(id != null)
{
cachedFetches.put(key, id);
return id;
}
}
catch(Exception e)
{
////////////////////////////////////////////////////////////////////
// assume this may mean a dupe-key - so - try another fetch below //
////////////////////////////////////////////////////////////////////
LOG.debug("Caught error inserting " + tableName + " named " + nameValue + " - will try to re-fetch", e);
}
id = fetchIdFromName(tableName, nameValue);
if(id != null)
{
cachedFetches.put(key, id);
return id;
}
/////////////
// give up //
/////////////
throw (new QException("Unable to get id for " + tableName + " named " + nameValue));
}
return (cachedFetches.get(key));
}
/*******************************************************************************
**
*******************************************************************************/
private Integer fetchIdFromName(String tableName, String nameValue) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(tableName);
getInput.setUniqueKey(Map.of("name", nameValue));
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
return (getOutput.getRecord().getValueInteger("id"));
}
return (null);
}
}

View File

@ -0,0 +1,556 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.audits;
import java.io.Serializable;
import java.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;
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.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.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditOutput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
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.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;
/*******************************************************************************
** Audit for a standard DML (Data Manipulation Language) activity - e.g.,
** insert, edit, or delete.
*******************************************************************************/
public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAuditOutput>
{
private static final QLogger LOG = QLogger.getLogger(DMLAuditAction.class);
public static final String AUDIT_CONTEXT_FIELD_NAME = "auditContext";
/*******************************************************************************
**
*******************************************************************************/
@Override
public DMLAuditOutput execute(DMLAuditInput input) throws QException
{
DMLAuditOutput output = new DMLAuditOutput();
AbstractTableActionInput tableActionInput = input.getTableActionInput();
List<QRecord> oldRecordList = input.getOldRecordList();
QTableMetaData table = tableActionInput.getTable();
long start = System.currentTimeMillis();
DMLType dmlType = getDMLType(tableActionInput);
try
{
List<QRecord> recordList = CollectionUtils.nonNullList(input.getRecordList()).stream()
.filter(r -> CollectionUtils.nullSafeIsEmpty(r.getErrors())).toList();
AuditLevel auditLevel = getAuditLevel(tableActionInput);
if(auditLevel == null || auditLevel.equals(AuditLevel.NONE) || CollectionUtils.nullSafeIsEmpty(recordList))
{
/////////////////////////////////////////////
// return with noop for null or level NONE //
/////////////////////////////////////////////
return (output);
}
String contextSuffix = getContentSuffix(input);
AuditInput auditInput = new AuditInput();
if(auditLevel.equals(AuditLevel.RECORD) || (auditLevel.equals(AuditLevel.FIELD) && !dmlType.supportsFields))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// make many simple audits (no details) for RECORD level //
// or for FIELD level, but on a DML type that doesn't support field-level details (e.g., DELETE or OTHER) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord record : recordList)
{
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))
{
Map<Serializable, QRecord> oldRecordMap = buildOldRecordMap(table, oldRecordList);
///////////////////////////////////////////////////////////////////
// do many audits, all with field level details, for FIELD level //
///////////////////////////////////////////////////////////////////
QPossibleValueTranslator qPossibleValueTranslator = new QPossibleValueTranslator(QContext.getQInstance(), QContext.getQSession());
qPossibleValueTranslator.translatePossibleValuesInRecords(table, CollectionUtils.mergeLists(recordList, oldRecordList));
//////////////////////////////////////////
// sort the field names by their labels //
//////////////////////////////////////////
List<String> sortedFieldNames = table.getFields().keySet().stream()
.sorted(Comparator.comparing(fieldName -> table.getFields().get(fieldName).getLabel()))
.toList();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
//////////////////////////////////////////////
// build single audit input for each record //
//////////////////////////////////////////////
for(QRecord record : recordList)
{
QRecord oldRecord = oldRecordMap.get(ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(primaryKeyField.getName())));
List<QRecord> details = new ArrayList<>();
for(String fieldName : sortedFieldNames)
{
makeAuditDetailRecordForField(fieldName, table, dmlType, record, oldRecord)
.ifPresent(details::add);
}
if(details.isEmpty() && DMLType.UPDATE.equals(dmlType))
{
// no, let's just noop.
// details.add(new QRecord().withValue("message", "No fields values were changed."));
}
else
{
AuditAction.appendToInput(auditInput, table.getName(), record.getValueInteger(table.getPrimaryKeyField()), getRecordSecurityKeyValues(table, record, Optional.ofNullable(oldRecord)), "Record was " + dmlType.pastTenseVerb + contextSuffix, details);
}
}
}
// new AuditAction().executeAsync(auditInput); // todo async??? maybe get that from rules???
new AuditAction().execute(auditInput);
long end = System.currentTimeMillis();
LOG.trace("Audit performance", logPair("auditLevel", String.valueOf(auditLevel)), logPair("recordCount", recordList.size()), logPair("millis", (end - start)));
}
catch(Exception e)
{
LOG.error("Error performing DML audit", e, logPair("type", String.valueOf(dmlType)), logPair("table", table.getName()));
}
return (output);
}
/*******************************************************************************
**
*******************************************************************************/
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);
}
/*******************************************************************************
**
*******************************************************************************/
private static String getFormattedValueForAuditDetail(QRecord record, String fieldName, QFieldMetaData field, Serializable value)
{
String formattedValue = null;
if(value != null)
{
if(field.getType().equals(QFieldType.DATE_TIME) && value instanceof Instant instant)
{
formattedValue = QValueFormatter.formatDateTimeWithZone(instant.atZone(ZoneId.of(Objects.requireNonNullElse(QContext.getQInstance().getDefaultTimeZoneId(), "UTC"))));
}
else if(record.getDisplayValue(fieldName) != null)
{
formattedValue = record.getDisplayValue(fieldName);
}
else
{
formattedValue = QValueFormatter.formatValue(field, value);
}
}
return formattedValue;
}
/*******************************************************************************
**
*******************************************************************************/
private static String formatFormattedValueForDetailMessage(QFieldMetaData field, String formattedValue)
{
if(formattedValue == null || "null".equals(formattedValue))
{
formattedValue = "--";
}
else
{
if(QFieldType.STRING.equals(field.getType()) || field.getPossibleValueSourceName() != null)
{
formattedValue = '"' + formattedValue + '"';
}
}
return (formattedValue);
}
/*******************************************************************************
**
*******************************************************************************/
private Map<Serializable, QRecord> buildOldRecordMap(QTableMetaData table, List<QRecord> oldRecordList)
{
Map<Serializable, QRecord> rs = new HashMap<>();
for(QRecord record : CollectionUtils.nonNullList(oldRecordList))
{
rs.put(record.getValue(table.getPrimaryKeyField()), record);
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private DMLType getDMLType(AbstractTableActionInput tableActionInput)
{
if(tableActionInput instanceof InsertInput)
{
return DMLType.INSERT;
}
else if(tableActionInput instanceof UpdateInput)
{
return DMLType.UPDATE;
}
else if(tableActionInput instanceof DeleteInput)
{
return DMLType.DELETE;
}
else
{
return DMLType.OTHER;
}
}
/*******************************************************************************
**
*******************************************************************************/
public static AuditLevel getAuditLevel(AbstractTableActionInput tableActionInput)
{
QTableMetaData table = tableActionInput.getTable();
if(table.getAuditRules() == null)
{
return (AuditLevel.NONE);
}
return (table.getAuditRules().getAuditLevel());
}
/*******************************************************************************
**
*******************************************************************************/
enum DMLType
{
INSERT("Inserted", true),
UPDATE("Edited", true),
DELETE("Deleted", false),
OTHER("Processed", false);
private final String pastTenseVerb;
private final boolean supportsFields;
/*******************************************************************************
**
*******************************************************************************/
DMLType(String pastTenseVerb, boolean supportsFields)
{
this.pastTenseVerb = pastTenseVerb;
this.supportsFields = supportsFields;
}
}
}

View File

@ -22,12 +22,20 @@
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;
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.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.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -38,8 +46,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEv
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -47,7 +53,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RecordAutomationStatusUpdater
{
private static final Logger LOG = LogManager.getLogger(RecordAutomationStatusUpdater.class);
private static final QLogger LOG = QLogger.getLogger(RecordAutomationStatusUpdater.class);
@ -55,7 +61,7 @@ public class RecordAutomationStatusUpdater
** for a list of records from a table, set their automation status - based on
** how the table is configured.
*******************************************************************************/
public static boolean setAutomationStatusInRecords(QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
public static boolean setAutomationStatusInRecords(QSession session, QTableMetaData table, List<QRecord> records, AutomationStatus automationStatus)
{
if(table == null || table.getAutomationDetails() == null || CollectionUtils.nullSafeIsEmpty(records))
{
@ -83,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;
@ -93,6 +103,12 @@ public class RecordAutomationStatusUpdater
{
for(QRecord record : records)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - seems like there's some case here, where if an order was in PENDING_INSERT, but then some other job updated the record, that we'd //
// lose that pending status, which would be a Bad Thing™... //
// problem is - we may not have the full record in here, so we can't necessarily check the record to see what status it's currently in... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
record.setValue(automationDetails.getStatusTracking().getFieldName(), automationStatus.getId());
// todo - another field - for the automation timestamp??
}
@ -108,20 +124,91 @@ 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))
{
return tableActions.stream().noneMatch(a -> TriggerEvent.POST_INSERT.equals(a.getTriggerEvent()));
if(tableActions.stream().anyMatch(a -> TriggerEvent.POST_INSERT.equals(a.getTriggerEvent())))
{
return (false);
}
else if(areThereTableTriggersForTable(table, TriggerEvent.POST_INSERT))
{
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))
{
return tableActions.stream().noneMatch(a -> TriggerEvent.POST_UPDATE.equals(a.getTriggerEvent()));
if(tableActions.stream().anyMatch(a -> TriggerEvent.POST_UPDATE.equals(a.getTriggerEvent())))
{
return (false);
}
else if(areThereTableTriggersForTable(table, TriggerEvent.POST_UPDATE))
{
return (false);
}
////////////////////////////////////////////////////////////////////////////////////////
// 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);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean areThereTableTriggersForTable(QTableMetaData table, TriggerEvent triggerEvent)
{
if(QContext.getQInstance().getTable(TableTrigger.TABLE_NAME) == null)
{
return (false);
}
return (false);
try
{
///////////////////
// todo - cache? //
///////////////////
CountInput countInput = new CountInput();
countInput.setTableName(TableTrigger.TABLE_NAME);
countInput.setFilter(new QQueryFilter(
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, table.getName()),
new QFilterCriteria(triggerEvent.equals(TriggerEvent.POST_INSERT) ? "postInsert" : "postUpdate", QCriteriaOperator.EQUALS, true)
));
CountOutput countOutput = new CountAction().execute(countInput);
return (countOutput.getCount() != null && countOutput.getCount() > 0);
}
catch(Exception e)
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the count query failed, we're a bit safer to err on the side of "yeah, there might be automations" //
///////////////////////////////////////////////////////////////////////////////////////////////////////////
return (true);
}
}
@ -135,11 +222,10 @@ public class RecordAutomationStatusUpdater
QTableAutomationDetails automationDetails = table.getAutomationDetails();
if(automationDetails != null && AutomationStatusTrackingType.FIELD_IN_TABLE.equals(automationDetails.getStatusTracking().getType()))
{
boolean didSetStatusField = setAutomationStatusInRecords(table, records, automationStatus);
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus);
if(didSetStatusField)
{
UpdateInput updateInput = new UpdateInput(instance);
updateInput.setSession(session);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
/////////////////////////////////////////////////////////////////////////////////////
@ -151,6 +237,7 @@ public class RecordAutomationStatusUpdater
.withValue(table.getPrimaryKeyField(), r.getValue(table.getPrimaryKeyField()))
.withValue(automationDetails.getStatusTracking().getFieldName(), r.getValue(automationDetails.getStatusTracking().getFieldName()))).toList());
updateInput.setAreAllValuesBeingUpdatedTheSame(true);
updateInput.setOmitDmlAudit(true);
new UpdateAction().execute(updateInput);
}

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.automation;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.RunAdHocRecordScriptAction;
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.scripts.RunAdHocRecordScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAdHocRecordScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
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;
/*******************************************************************************
**
*******************************************************************************/
public class RunRecordScriptAutomationHandler extends RecordAutomationHandler
{
private static final QLogger LOG = QLogger.getLogger(RunRecordScriptAutomationHandler.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void execute(RecordAutomationInput recordAutomationInput) throws QException
{
String tableName = recordAutomationInput.getTableName();
Map<String, Serializable> values = recordAutomationInput.getAction().getValues();
Integer scriptId = ValueUtils.getValueAsInteger(values.get("scriptId"));
if(scriptId == null)
{
throw (new QException("ScriptId was not provided in values map for record automations on table: " + tableName));
}
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(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeIsEmpty(queryOutput.getRecords()))
{
throw (new QException("Could not find current revision for scriptId: " + scriptId + " on table " + tableName));
}
QRecord scriptRevision = queryOutput.getRecords().get(0);
LOG.info("Running script against records", logPair("scriptRevisionId", scriptRevision.getValue("id")), logPair("scriptId", scriptRevision.getValue("scriptIdd")));
RunAdHocRecordScriptInput input = new RunAdHocRecordScriptInput();
input.setCodeReference(new AdHocScriptCodeReference().withScriptRevisionRecord(scriptRevision));
input.setTableName(tableName);
input.setRecordList(recordAutomationInput.getRecordList());
RunAdHocRecordScriptOutput output = new RunAdHocRecordScriptOutput();
new RunAdHocRecordScriptAction().run(input, output);
}
}

View File

@ -25,41 +25,53 @@ package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.automation.RunRecordScriptAutomationHandler;
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;
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.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -73,7 +85,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class PollingAutomationPerTableRunner implements Runnable
{
private static final Logger LOG = LogManager.getLogger(PollingAutomationPerTableRunner.class);
private static final QLogger LOG = QLogger.getLogger(PollingAutomationPerTableRunner.class);
private final TableActions tableActions;
private final String name;
@ -86,6 +98,11 @@ public class PollingAutomationPerTableRunner implements Runnable
TriggerEvent.POST_UPDATE, AutomationStatus.PENDING_UPDATE_AUTOMATIONS
);
private static Map<AutomationStatus, TriggerEvent> automationStatusTriggerEventMap = Map.of(
AutomationStatus.PENDING_INSERT_AUTOMATIONS, TriggerEvent.POST_INSERT,
AutomationStatus.PENDING_UPDATE_AUTOMATIONS, TriggerEvent.POST_UPDATE
);
private static Map<AutomationStatus, AutomationStatus> pendingToRunningStatusMap = Map.of(
AutomationStatus.PENDING_INSERT_AUTOMATIONS, AutomationStatus.RUNNING_INSERT_AUTOMATIONS,
AutomationStatus.PENDING_UPDATE_AUTOMATIONS, AutomationStatus.RUNNING_UPDATE_AUTOMATIONS
@ -101,51 +118,26 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
**
*******************************************************************************/
public record TableActions(String tableName, AutomationStatus status, List<TableAutomationAction> actions)
public record TableActions(String tableName, AutomationStatus status)
{
}
/*******************************************************************************
**
** basically just get a list of tables which at least *could* have automations
** run - either meta-data automations, or table-triggers (data/user defined).
*******************************************************************************/
public static List<TableActions> getTableActions(QInstance instance, String providerName)
{
Map<String, Map<AutomationStatus, List<TableAutomationAction>>> workingTableActionMap = new HashMap<>();
List<TableActions> tableActionList = new ArrayList<>();
List<TableActions> tableActionList = new ArrayList<>();
//////////////////////////////////////////////////////////////////////
// todo - share logic like this among any automation implementation //
//////////////////////////////////////////////////////////////////////
for(QTableMetaData table : instance.getTables().values())
{
if(table.getAutomationDetails() != null && providerName.equals(table.getAutomationDetails().getProviderName()))
{
///////////////////////////////////////////////////////////////////////////
// organize the table's actions by type //
// todo - in future, need user-defined actions here too (and refreshed!) //
///////////////////////////////////////////////////////////////////////////
for(TableAutomationAction action : table.getAutomationDetails().getActions())
{
AutomationStatus automationStatus = triggerEventAutomationStatusMap.get(action.getTriggerEvent());
workingTableActionMap.putIfAbsent(table.getName(), new HashMap<>());
workingTableActionMap.get(table.getName()).putIfAbsent(automationStatus, new ArrayList<>());
workingTableActionMap.get(table.getName()).get(automationStatus).add(action);
}
////////////////////////////////////////////
// convert the map to tableAction records //
////////////////////////////////////////////
for(Map.Entry<AutomationStatus, List<TableAutomationAction>> entry : workingTableActionMap.get(table.getName()).entrySet())
{
AutomationStatus automationStatus = entry.getKey();
List<TableAutomationAction> actionList = entry.getValue();
actionList.sort(Comparator.comparing(TableAutomationAction::getPriority));
tableActionList.add(new TableActions(table.getName(), automationStatus, actionList));
}
tableActionList.add(new TableActions(table.getName(), AutomationStatus.PENDING_INSERT_AUTOMATIONS));
tableActionList.add(new TableActions(table.getName(), AutomationStatus.PENDING_UPDATE_AUTOMATIONS));
}
}
@ -173,18 +165,26 @@ public class PollingAutomationPerTableRunner implements Runnable
@Override
public void run()
{
QContext.init(instance, sessionSupplier.get());
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
LOG.info("Running " + this.getClass().getSimpleName() + "[" + name + "]");
LOG.debug("Running " + this.getClass().getSimpleName() + "[" + name + "]");
try
{
QSession session = sessionSupplier != null ? sessionSupplier.get() : new QSession();
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status(), tableActions.actions());
processTableInsertOrUpdate(instance.getTable(tableActions.tableName()), session, tableActions.status());
}
catch(Exception e)
{
LOG.warn("Error running automations", e);
}
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}
@ -192,8 +192,12 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
*******************************************************************************/
private void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus, List<TableAutomationAction> actions) throws QException
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus) throws QException
{
/////////////////////////////////////////////////////////////////////////
// get the actions to run against this table in this automation status //
/////////////////////////////////////////////////////////////////////////
List<TableAutomationAction> actions = getTableActions(table, automationStatus);
if(CollectionUtils.nullSafeIsEmpty(actions))
{
return;
@ -204,18 +208,21 @@ public class PollingAutomationPerTableRunner implements Runnable
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// run an async-pipe loop - that will query for records in PENDING - put them in a pipe - then apply actions to them //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new RecordPipe();
AsyncRecordPipeLoop asyncRecordPipeLoop = new AsyncRecordPipeLoop();
asyncRecordPipeLoop.run("PollingAutomationRunner>Query>" + automationStatus, null, recordPipe, (status) ->
QTableAutomationDetails automationDetails = table.getAutomationDetails();
AsyncRecordPipeLoop asyncRecordPipeLoop = new AsyncRecordPipeLoop();
RecordPipe recordPipe = automationDetails.getOverrideBatchSize() == null
? new RecordPipe() : new RecordPipe(automationDetails.getOverrideBatchSize());
asyncRecordPipeLoop.run("PollingAutomationRunner>Query>" + automationStatus + ">" + table.getName(), null, recordPipe, (status) ->
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
AutomationStatusTrackingType statusTrackingType = table.getAutomationDetails().getStatusTracking().getType();
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
if(AutomationStatusTrackingType.FIELD_IN_TABLE.equals(statusTrackingType))
{
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(table.getAutomationDetails().getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(automationDetails.getStatusTracking().getFieldName(), QCriteriaOperator.EQUALS, List.of(automationStatus.getId()))));
}
else
{
@ -235,6 +242,83 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** get the actions to run against a table in an automation status. both from
** metaData and tableTriggers/data.
*******************************************************************************/
private List<TableAutomationAction> getTableActions(QTableMetaData table, AutomationStatus automationStatus) throws QException
{
List<TableAutomationAction> rs = new ArrayList<>();
TriggerEvent triggerEvent = automationStatusTriggerEventMap.get(automationStatus);
///////////////////////////////////////////////////////////
// start with any actions defined in the table meta data //
///////////////////////////////////////////////////////////
for(TableAutomationAction action : table.getAutomationDetails().getActions())
{
if(action.getTriggerEvent().equals(triggerEvent))
{
rs.add(action);
}
}
/////////////////////////////////////////////////
// next add any tableTriggers, defined in data //
/////////////////////////////////////////////////
if(QContext.getQInstance().getTable(TableTrigger.TABLE_NAME) != null)
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TableTrigger.TABLE_NAME);
queryInput.setFilter(new QQueryFilter(
new QFilterCriteria("tableName", QCriteriaOperator.EQUALS, table.getName()),
new QFilterCriteria(triggerEvent.equals(TriggerEvent.POST_INSERT) ? "postInsert" : "postUpdate", QCriteriaOperator.EQUALS, true)
));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
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()));
}
}
}
rs.sort(Comparator.comparing(taa -> Objects.requireNonNullElse(taa.getPriority(), Integer.MAX_VALUE)));
return (rs);
}
/*******************************************************************************
** For a set of records that were found to be in a PENDING state - run all the
** table's actions against them - IF they are found to match the action's filter
@ -258,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(session, table, records, action);
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(session, table, matchingQRecords, action);
}
}
catch(Exception e)
{
LOG.warn("Caught exception processing records on " + table + " for action " + action, e);
anyActionsFailed = true;
}
}
@ -293,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).
@ -305,32 +407,34 @@ public class PollingAutomationPerTableRunner implements Runnable
** but that will almost certainly give potentially different results than a true
** backend - e.g., just consider if the DB is case-sensitive for strings...
*******************************************************************************/
private List<QRecord> getRecordsMatchingActionFilter(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
private List<QRecord> getRecordsMatchingActionFilter(QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
///////////////////////////////////////////////////////////////////////////////////////
// set up a filter that is for the primary keys IN the list that we identified above //
///////////////////////////////////////////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
filter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList()));
/////////////////////////////////////////////////////////////////////////////////////////////////////
// copy filter criteria from the action's filter to a new filter that we'll run here. //
// Critically - don't modify the filter object on the action! as that object has a long lifespan. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
if(action.getFilter() != null)
{
if(action.getFilter().getCriteria() != null)
{
action.getFilter().getCriteria().forEach(filter::addCriteria);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
// if the action defines a filter of its own, add that to the filter we'll run now as a sub-filter //
// not entirely clear if this needs to be a clone, but, it feels safe and cheap enough //
/////////////////////////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(action.getFilter().clone());
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// we also want to set order-bys from the action into our filter (since they only apply at the top-level) //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(action.getFilter().getOrderBys() != null)
{
action.getFilter().getOrderBys().forEach(filter::addOrderBy);
}
}
filter.addCriteria(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, records.stream().map(r -> r.getValue(table.getPrimaryKeyField())).toList()));
/////////////////////////////////////////////////////////////////////////////////////////////
// always add order-by the primary key, to give more predictable/consistent results //
// todo - in future - if this becomes a source of slowness, make this a config to opt-out? //
@ -339,6 +443,8 @@ public class PollingAutomationPerTableRunner implements Runnable
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(action.getIncludeRecordAssociations());
return (new QueryAction().execute(queryInput).getRecords());
}
@ -346,8 +452,9 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Finally, actually run action code against a list of known matching records.
** todo not commit - move to somewhere genericer
*******************************************************************************/
private void applyActionToMatchingRecords(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
public static void applyActionToMatchingRecords(QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws Exception
{
if(StringUtils.hasContent(action.getProcessName()))
{
@ -356,8 +463,7 @@ public class PollingAutomationPerTableRunner implements Runnable
// tell it to SKIP frontend steps. //
// give the process a callback w/ a query filter that has the p-keys of these records. //
/////////////////////////////////////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput(instance);
runProcessInput.setSession(session);
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(action.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.setCallback(new QProcessCallback()
@ -370,20 +476,29 @@ public class PollingAutomationPerTableRunner implements Runnable
}
});
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
if(runProcessOutput.getException().isPresent())
try
{
throw (runProcessOutput.getException().get());
QContext.pushAction(runProcessInput);
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
if(runProcessOutput.getException().isPresent())
{
throw (runProcessOutput.getException().get());
}
}
finally
{
QContext.popAction();
}
}
else if(action.getCodeReference() != null)
{
LOG.debug(" Executing action: [" + action.getName() + "] as code reference: " + action.getCodeReference());
RecordAutomationInput input = new RecordAutomationInput(instance);
input.setSession(session);
RecordAutomationInput input = new RecordAutomationInput();
input.setTableName(table.getName());
input.setRecordList(records);
input.setAction(action);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
recordAutomationHandler.execute(input);

View File

@ -0,0 +1,82 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Abstract class that a table can specify an implementation of, to provide
** custom actions after a delete takes place.
**
** General implementation would be, to iterate over the records (ones which didn't
** have a delete error), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
** - possibly throwing an exception - though doing so won't stop the delete, and instead
** will just set a warning on all of the deleted records...
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go back
** to the caller - this is how errors and warnings are propagated .
**
** Note that the full deleteInput is available as a field in this class.
**
** A future enhancement here may be to take (as fields in this class) the list of
** records that the delete action marked in error - the user might want to do
** something special with them (idk, try some other way to delete them?)
*******************************************************************************/
public abstract class AbstractPostDeleteCustomizer
{
protected DeleteInput deleteInput;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
/*******************************************************************************
** Getter for deleteInput
**
*******************************************************************************/
public DeleteInput getDeleteInput()
{
return deleteInput;
}
/*******************************************************************************
** Setter for deleteInput
**
*******************************************************************************/
public void setDeleteInput(DeleteInput deleteInput)
{
this.deleteInput = deleteInput;
}
}

View File

@ -0,0 +1,77 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Abstract class that a table can specify an implementation of, to provide
** custom actions after an insert takes place.
**
** General implementation would be, to iterate over the records (the outputs of
** the insert action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly throwing an exception - though doing so won't stop the update, and instead
** will just set a warning on all of the updated records...
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go back to the caller.
**
** Note that the full insertInput is available as a field in this class.
*******************************************************************************/
public abstract class AbstractPostInsertCustomizer
{
protected InsertInput insertInput;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
/*******************************************************************************
** Getter for insertInput
**
*******************************************************************************/
public InsertInput getInsertInput()
{
return insertInput;
}
/*******************************************************************************
** Setter for insertInput
**
*******************************************************************************/
public void setInsertInput(InsertInput insertInput)
{
this.insertInput = insertInput;
}
}

View File

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

View File

@ -0,0 +1,130 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Abstract class that a table can specify an implementation of, to provide
** custom actions after an update takes place.
**
** General implementation would be, to iterate over the records (the outputs of
** the update action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records?
** - possibly throwing an exception - though doing so won't stop the update, and instead
** will just set a warning on all of the updated records...
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go back to the caller.
**
** Note that the full updateInput is available as a field in this class, and the
** "old records" (e.g., with values freshly fetched from the backend) will be
** available (if the backend supports it) - both as a list (`getOldRecordList`)
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
*******************************************************************************/
public abstract class AbstractPostUpdateCustomizer
{
protected UpdateInput updateInput;
protected List<QRecord> oldRecordList;
private Map<Serializable, QRecord> oldRecordMap = null;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
/*******************************************************************************
** Getter for updateInput
**
*******************************************************************************/
public UpdateInput getUpdateInput()
{
return updateInput;
}
/*******************************************************************************
** Setter for updateInput
**
*******************************************************************************/
public void setUpdateInput(UpdateInput updateInput)
{
this.updateInput = updateInput;
}
/*******************************************************************************
**
*******************************************************************************/
public void setOldRecordList(List<QRecord> oldRecordList)
{
this.oldRecordList = oldRecordList;
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> getOldRecordList()
{
return oldRecordList;
}
/*******************************************************************************
**
*******************************************************************************/
protected Map<Serializable, QRecord> getOldRecordMap()
{
if(oldRecordMap == null)
{
oldRecordMap = new HashMap<>();
if(oldRecordList != null && updateInput != null)
{
for(QRecord qRecord : oldRecordList)
{
oldRecordMap.put(qRecord.getValue(updateInput.getTable().getPrimaryKeyField()), qRecord);
}
}
}
return (oldRecordMap);
}
}

View File

@ -0,0 +1,119 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Abstract class that a table can specify an implementation of, to provide
** custom actions before a delete takes place.
**
** It's important for implementations to be aware of the isPreview field, which
** is set to true when the code is running to give users advice, e.g., on a review
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
** things like storing data, you don't want to do that if isPreview is true!!
**
** General implementation would be, to iterate over the records (which the DeleteAction
** would look up based on the inputs to the delete action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly throwing an exception - if you really don't want the delete operation to continue.
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) - this is how errors
** and warnings are propagated to the DeleteAction. Note that any records with
** an error will NOT proceed to the backend's delete interface - but those with
** warnings will.
**
** Note that the full deleteInput is available as a field in this class.
**
*******************************************************************************/
public abstract class AbstractPreDeleteCustomizer
{
protected DeleteInput deleteInput;
protected boolean isPreview = false;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
/*******************************************************************************
** Getter for deleteInput
**
*******************************************************************************/
public DeleteInput getDeleteInput()
{
return deleteInput;
}
/*******************************************************************************
** Setter for deleteInput
**
*******************************************************************************/
public void setDeleteInput(DeleteInput deleteInput)
{
this.deleteInput = deleteInput;
}
/*******************************************************************************
** Getter for isPreview
*******************************************************************************/
public boolean getIsPreview()
{
return (this.isPreview);
}
/*******************************************************************************
** Setter for isPreview
*******************************************************************************/
public void setIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
}
/*******************************************************************************
** Fluent setter for isPreview
*******************************************************************************/
public AbstractPreDeleteCustomizer withIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
return (this);
}
}

View File

@ -0,0 +1,116 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Abstract class that a table can specify an implementation of, to provide
** custom actions before an insert takes place.
**
** It's important for implementations to be aware of the isPreview field, which
** is set to true when the code is running to give users advice, e.g., on a review
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
** things like storing data, you don't want to do that if isPreview is true!!
**
** General implementation would be, to iterate over the records (the inputs to
** the insert action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly manipulating values (`setValue`)
** - possibly throwing an exception - if you really don't want the insert operation to continue.
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
**
** Note that the full insertInput is available as a field in this class.
*******************************************************************************/
public abstract class AbstractPreInsertCustomizer
{
protected InsertInput insertInput;
protected boolean isPreview = false;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
/*******************************************************************************
** Getter for insertInput
**
*******************************************************************************/
public InsertInput getInsertInput()
{
return insertInput;
}
/*******************************************************************************
** Setter for insertInput
**
*******************************************************************************/
public void setInsertInput(InsertInput insertInput)
{
this.insertInput = insertInput;
}
/*******************************************************************************
** Getter for isPreview
*******************************************************************************/
public boolean getIsPreview()
{
return (this.isPreview);
}
/*******************************************************************************
** Setter for isPreview
*******************************************************************************/
public void setIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
}
/*******************************************************************************
** Fluent setter for isPreview
*******************************************************************************/
public AbstractPreInsertCustomizer withIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
return (this);
}
}

View File

@ -0,0 +1,163 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Abstract class that a table can specify an implementation of, to provide
** custom actions before an update takes place.
**
** It's important for implementations to be aware of the isPreview field, which
** is set to true when the code is running to give users advice, e.g., on a review
** screen - vs. being false when the action is ACTUALLY happening. So, if you're doing
** things like storing data, you don't want to do that if isPreview is true!!
**
** General implementation would be, to iterate over the records (the inputs to
** the update action), and look at their values:
** - possibly adding Errors (`addError`) or Warnings (`addWarning`) to the records
** - possibly manipulating values (`setValue`)
** - possibly throwing an exception - if you really don't want the update operation to continue.
** - doing "whatever else" you may want to do.
** - returning the list of records (can be the input list) that you want to go on to the backend implementation class.
**
** Note that the full updateInput is available as a field in this class, and the
** "old records" (e.g., with values freshly fetched from the backend) will be
** available (if the backend supports it) - both as a list (`getOldRecordList`)
** and as a memoized (by this class) map of primaryKey to record (`getOldRecordMap`).
*******************************************************************************/
public abstract class AbstractPreUpdateCustomizer
{
protected UpdateInput updateInput;
protected List<QRecord> oldRecordList;
protected boolean isPreview = false;
private Map<Serializable, QRecord> oldRecordMap = null;
/*******************************************************************************
**
*******************************************************************************/
public abstract List<QRecord> apply(List<QRecord> records) throws QException;
/*******************************************************************************
** Getter for updateInput
**
*******************************************************************************/
public UpdateInput getUpdateInput()
{
return updateInput;
}
/*******************************************************************************
** Setter for updateInput
**
*******************************************************************************/
public void setUpdateInput(UpdateInput updateInput)
{
this.updateInput = updateInput;
}
/*******************************************************************************
**
*******************************************************************************/
public void setOldRecordList(List<QRecord> oldRecordList)
{
this.oldRecordList = oldRecordList;
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> getOldRecordList()
{
return oldRecordList;
}
/*******************************************************************************
**
*******************************************************************************/
protected Map<Serializable, QRecord> getOldRecordMap()
{
if(oldRecordMap == null)
{
oldRecordMap = new HashMap<>();
for(QRecord qRecord : oldRecordList)
{
oldRecordMap.put(qRecord.getValue(updateInput.getTable().getPrimaryKeyField()), qRecord);
}
}
return (oldRecordMap);
}
/*******************************************************************************
** Getter for isPreview
*******************************************************************************/
public boolean getIsPreview()
{
return (this.isPreview);
}
/*******************************************************************************
** Setter for isPreview
*******************************************************************************/
public void setIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
}
/*******************************************************************************
** Fluent setter for isPreview
*******************************************************************************/
public AbstractPreUpdateCustomizer withIsPreview(boolean isPreview)
{
this.isPreview = isPreview;
return (this);
}
}

View File

@ -0,0 +1,232 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.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;
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.statusmessages.QStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
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". Optionally (based on RelationshipType), there can be a foreign
** key in "parent", pointed at "child". e.g., named: "parent.childId".
**
*******************************************************************************/
public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInsertCustomizer
{
public enum RelationshipType
{
PARENT_POINTS_AT_CHILD,
CHILD_POINTS_AT_PARENT
}
/*******************************************************************************
**
*******************************************************************************/
public abstract QRecord buildChildForRecord(QRecord parentRecord) throws QException;
/*******************************************************************************
**
*******************************************************************************/
public abstract String getChildTableName();
/*******************************************************************************
**
*******************************************************************************/
public String getForeignKeyFieldName()
{
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
public abstract RelationshipType getRelationshipType();
/*******************************************************************************
**
*******************************************************************************/
@Override
public List<QRecord> apply(List<QRecord> records)
{
try
{
List<QRecord> rs = records;
List<QRecord> childrenToInsert = new ArrayList<>();
QTableMetaData table = getInsertInput().getTable();
QTableMetaData childTable = getInsertInput().getInstance().getTable(getChildTableName());
////////////////////////////////////////////////////////////////////////////////
// iterate over the inserted records, building a list child records to insert //
// for ones missing a value in the foreign key field. //
////////////////////////////////////////////////////////////////////////////////
switch(getRelationshipType())
{
case PARENT_POINTS_AT_CHILD ->
{
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());
}
///////////////////////////////////////////////////////////////////////////////////
// if there are no children to insert, then just return the original record list //
///////////////////////////////////////////////////////////////////////////////////
if(childrenToInsert.isEmpty())
{
return (records);
}
/////////////////////////
// insert the children //
/////////////////////////
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getChildTableName());
insertInput.setRecords(childrenToInsert);
insertInput.setTransaction(this.insertInput.getTransaction());
InsertOutput insertOutput = new InsertAction().execute(insertInput);
Iterator<QRecord> insertedRecordIterator = insertOutput.getRecords().iterator();
/////////////////////////////////////////////////////////////////////////////////
// check for any errors when inserting the children, if any errors were found, //
// then set a warning in the parent with the details of the problem //
/////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
// 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. //
//////////////////////////////////////////////////////////////////////////////////////////////////////
switch(getRelationshipType())
{
case PARENT_POINTS_AT_CHILD ->
{
rs = new ArrayList<>();
List<QRecord> recordsToUpdate = new ArrayList<>();
for(QRecord record : records)
{
Serializable primaryKey = record.getValue(table.getPrimaryKeyField());
if(record.getValue(getForeignKeyFieldName()) == null)
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
}
}
////////////////////////////////////////////////////////////////////////////
// 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);
}
case CHILD_POINTS_AT_PARENT ->
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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());
}
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

@ -28,13 +28,13 @@ import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -42,7 +42,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QCodeLoader
{
private static final Logger LOG = LogManager.getLogger(QCodeLoader.class);
private static final QLogger LOG = QLogger.getLogger(QCodeLoader.class);
@ -61,6 +61,21 @@ public class QCodeLoader
/*******************************************************************************
**
*******************************************************************************/
public static <T> Optional<T> getTableCustomizer(Class<T> expectedClass, QTableMetaData table, String customizerName)
{
Optional<QCodeReference> codeReference = table.getCustomizer(customizerName);
if(codeReference.isPresent())
{
return (Optional.ofNullable(QCodeLoader.getAdHoc(expectedClass, codeReference.get())));
}
return (Optional.empty());
}
/*******************************************************************************
**
*******************************************************************************/
@ -87,7 +102,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -126,7 +141,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //
@ -165,7 +180,7 @@ public class QCodeLoader
}
catch(Exception e)
{
LOG.error("Error initializing customizer: " + codeReference);
LOG.error("Error initializing customizer", logPair("codeReference", codeReference), e);
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// return null here - under the assumption that during normal run-time operations, we'll never hit here //

View File

@ -0,0 +1,149 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Interface with utility methods that pre insert/update/delete customizers
** may want to use.
*******************************************************************************/
public interface RecordCustomizerUtilityInterface
{
QLogger LOG = QLogger.getLogger(RecordCustomizerUtilityInterface.class);
/*******************************************************************************
** Container for an old value and a new value.
*******************************************************************************/
@SuppressWarnings("checkstyle:MethodName")
record Change(Serializable oldValue, Serializable newValue)
{
}
/*******************************************************************************
**
*******************************************************************************/
default Map<String, Change> getChanges(String tableName, QRecord oldRecord, QRecord newRecord)
{
Map<String, Change> rs = new HashMap<>();
QTableMetaData table = QContext.getQInstance().getTable(tableName);
for(Map.Entry<String, Serializable> entry : newRecord.getValues().entrySet())
{
String fieldName = entry.getKey();
Serializable newValue = entry.getValue();
Serializable oldValue = oldRecord.getValue(fieldName);
try
{
QFieldMetaData field = table.getField(fieldName);
Serializable newTypedValue = ValueUtils.getValueAsFieldType(field.getType(), newValue);
Serializable oldTypedValue = ValueUtils.getValueAsFieldType(field.getType(), oldValue);
if(!Objects.equals(oldTypedValue, newTypedValue))
{
rs.put(fieldName, new Change(oldTypedValue, newTypedValue));
}
}
catch(Exception e)
{
LOG.info("Error getting a value as field's type", e, logPair("fieldName", fieldName), logPair("oldValue", oldValue), logPair("newValue", newValue));
}
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
default void errorIfNoValue(Serializable value, QRecord record, String errorMessage)
{
errorIf(!StringUtils.hasContent(ValueUtils.getValueAsString(value)), record, errorMessage);
}
/*******************************************************************************
**
*******************************************************************************/
default void errorIfEditedValue(QRecord oldRecord, QRecord newRecord, String fieldName, String errorMessage)
{
if(newRecord.getValues().containsKey(fieldName))
{
errorIf(isChangedValue(oldRecord.getValue(fieldName), newRecord.getValue(fieldName)), newRecord, errorMessage);
}
}
/*******************************************************************************
**
*******************************************************************************/
default boolean isChangedValue(Serializable oldValue, Serializable newValue)
{
//////////////////////////////////////////////
// todo - probably ... some type "coercion" //
//////////////////////////////////////////////
return (!Objects.equals(oldValue, newValue));
}
/*******************************************************************************
**
*******************************************************************************/
default void errorIfAnyValue(Serializable value, QRecord record, String errorMessage)
{
if(StringUtils.hasContent(ValueUtils.getValueAsString(value)))
{
record.addError(new BadInputStatusMessage(errorMessage));
}
}
/*******************************************************************************
**
*******************************************************************************/
default void errorIf(boolean condition, QRecord record, String errorMessage)
{
if(condition)
{
record.addError(new BadInputStatusMessage(errorMessage));
}
}
}

View File

@ -22,45 +22,34 @@
package com.kingsrook.qqq.backend.core.actions.customizers;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Enum definition of possible table customizers - "roles" for custom code that
** can be applied to tables.
**
** Works with TableCustomizer (singular version of this name) objects, during
** instance validation, to provide validation of the referenced code (and to
** make such validation from sub-backend-modules possible in the future).
**
** The idea of the 3rd argument here is to provide a way that we can enforce
** the type-parameters for the custom code. E.g., if it's a Function - how
** can we check at run-time that the type-params are correct? We couldn't find
** how to do this "reflectively", so we can instead try to run the custom code,
** passing it objects of the type that this customizer expects, and a validation
** error will raise upon ClassCastException... This maybe could improve!
*******************************************************************************/
public enum TableCustomizers
{
POST_QUERY_RECORD(new TableCustomizer("postQueryRecord", Function.class, ((Object x) ->
{
@SuppressWarnings("unchecked")
Function<QRecord, QRecord> function = (Function<QRecord, QRecord>) x;
QRecord output = function.apply(new QRecord());
})));
POST_QUERY_RECORD("postQueryRecord", AbstractPostQueryCustomizer.class),
PRE_INSERT_RECORD("preInsertRecord", AbstractPreInsertCustomizer.class),
POST_INSERT_RECORD("postInsertRecord", AbstractPostInsertCustomizer.class),
PRE_UPDATE_RECORD("preUpdateRecord", AbstractPreUpdateCustomizer.class),
POST_UPDATE_RECORD("postUpdateRecord", AbstractPostUpdateCustomizer.class),
PRE_DELETE_RECORD("preDeleteRecord", AbstractPreDeleteCustomizer.class),
POST_DELETE_RECORD("postDeleteRecord", AbstractPostDeleteCustomizer.class);
private final TableCustomizer tableCustomizer;
private final String role;
private final Class<?> expectedType;
/*******************************************************************************
**
*******************************************************************************/
TableCustomizers(TableCustomizer tableCustomizer)
TableCustomizers(String role, Class<?> expectedType)
{
this.tableCustomizer = tableCustomizer;
this.role = role;
this.expectedType = expectedType;
}
@ -73,7 +62,7 @@ public enum TableCustomizers
{
for(TableCustomizers value : values())
{
if(value.tableCustomizer.getRole().equals(name))
if(value.role.equals(name))
{
return (value);
}
@ -84,24 +73,23 @@ public enum TableCustomizers
/*******************************************************************************
** Getter for tableCustomizer
**
*******************************************************************************/
public TableCustomizer getTableCustomizer()
{
return tableCustomizer;
}
/*******************************************************************************
** get the role from the tableCustomizer
**
*******************************************************************************/
public String getRole()
{
return (tableCustomizer.getRole());
return (role);
}
/*******************************************************************************
** Getter for expectedType
**
*******************************************************************************/
public Class<?> getExpectedType()
{
return expectedType;
}
}

View File

@ -0,0 +1,422 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
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.actions.dashboard.widgets.AbstractWidgetRenderer;
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.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Base class for rendering qqq HTML dashboard widgets
**
*******************************************************************************/
public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
public static String openTopLevelBulletList()
{
return ("""
<div style="padding-left: 2rem;">
<ul>""");
}
/*******************************************************************************
**
*******************************************************************************/
public static String closeTopLevelBulletList()
{
return ("""
</ul>
</div>""");
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletItalics(String text)
{
return ("<li><i>" + text + "</i></li>");
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletLink(String href, String text)
{
return ("<li><a href=\"" + href + "\">" + text + "</a></li>");
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletNameLink(String name, String href, String text)
{
return (bulletNameValue(name, "<a href=\"" + href + "\">" + text + "</a>"));
}
/*******************************************************************************
**
*******************************************************************************/
protected String bulletNameValue(String name, String value)
{
return ("<li><b>" + name + "</b> &nbsp; " + Objects.requireNonNullElse(value, "--") + "</li>");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableBulkLoad(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + tableName + ".bulkInsert");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableBulkLoadChildren(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
}
return ("#/launchProcess=" + tableName + ".bulkInsert");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreate(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create?defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), Charset.defaultCharset()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String getCountLink(RenderWidgetInput input, String tableName, QQueryFilter filter, int count) throws QException
{
String totalString = QValueFormatter.formatValue(DisplayFormat.COMMAS, count);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null || filter == null)
{
return (totalString);
}
return ("<a href='" + tablePath + "?filter=" + JsonUtils.toJson(filter) + "'>" + totalString + "</a>");
}
/*******************************************************************************
**
*******************************************************************************/
public static void addTableFilterToListIfPermissed(RenderWidgetInput input, String tableName, List<String> urls, QQueryFilter filter) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return;
}
urls.add(tablePath + "?filter=" + JsonUtils.toJson(filter));
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableFilterUnencoded(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
}
return (tablePath + "?filter=" + JsonUtils.toJson(filter));
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableFilter(String tableName, QQueryFilter filter) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
}
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableFilterNoOfRecords(String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException
{
return (aHrefTableFilterNoOfRecords(tableName, filter, noOfRecords, singularLabel, pluralLabel, false));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableFilterNoOfRecords(String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel, boolean onlyLinkCount) throws QException
{
String plural = StringUtils.plural(noOfRecords, singularLabel, pluralLabel);
String countString = QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords);
String displayText = StringUtils.hasContent(plural) ? (" " + plural) : "";
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (countString + displayText);
}
String href = linkTableFilter(tableName, filter);
if(onlyLinkCount)
{
return ("<a href=\"" + href + "\">" + countString + "</a>" + displayText);
}
else
{
return ("<a href=\"" + href + "\">" + countString + displayText + "</a>");
}
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefViewRecord(String tableName, Serializable id, String linkText) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (linkText);
}
return ("<a href=\"" + linkRecordView(tableName, id) + "\">" + linkText + "</a>");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/edit");
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkRecordView(String tableName, Serializable recordId) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
}
return (tablePath + "/" + recordId);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForFilter(AbstractActionInput input, String processName, QQueryFilter filter) throws QException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process == null)
{
return (null);
}
String tableName = process.getTableName();
if(tableName == null)
{
return (null);
}
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + processName + "?recordsParam=filterJSON&filterJSON=" + URLEncoder.encode(JsonUtils.toJson(filter), StandardCharsets.UTF_8));
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkProcessForRecord(AbstractActionInput input, String processName, Serializable recordId) throws QException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
String tableName = process.getTableName();
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/" + processName);
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues) throws QException
{
return (linkTableCreateChild(input, childTableName, defaultValues, defaultValues.keySet()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues) throws QException
{
return (aHrefTableCreateChild(input, childTableName, defaultValues, defaultValues.keySet()));
}
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(childTableName);
if(tablePath == null)
{
return (null);
}
Map<String, Integer> disabledFieldsMap = disabledFields.stream().collect(Collectors.toMap(k -> k, k -> 1));
return ("#/createChild=" + childTableName
+ "/defaultValues=" + URLEncoder.encode(JsonUtils.toJson(defaultValues), StandardCharsets.UTF_8).replaceAll("\\+", "%20")
+ "/disabledFields=" + URLEncoder.encode(JsonUtils.toJson(disabledFieldsMap), StandardCharsets.UTF_8).replaceAll("\\+", "%20"));
}
/*******************************************************************************
**
*******************************************************************************/
public static String getChipElement(String icon, String label, String color) throws QException
{
color = color != null ? color : "info";
color = StringUtils.ucFirst(color);
String html = "<span style='display: flex;'>";
html += "<div style='overflow: hidden; flex: none; display: flex; align-content: flex-start; align-items: center; height: 24px; padding-right: 8px; font-size: 13px; font-weight: 500; border: 1px solid; border-radius: 16px; color: " + color + "'>";
if(icon != null)
{
html += "<span style='font-size: 16px; padding: 5px' class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit MuiChip-icon MuiChip-iconSmall MuiChip-iconColor" + color + "'>" + icon + "</span>";
}
html += "<span class='MuiChip-label MuiChip-labelSmall'>" + label + "</span></div></span>";
return (html);
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(childTableName);
if(tablePath == null)
{
return (null);
}
return ("<a href=\"" + linkTableCreateChild(input, childTableName, defaultValues, defaultValues.keySet()) + "\">Create new</a>");
}
}

View File

@ -22,11 +22,15 @@
package com.kingsrook.qqq.backend.core.actions.dashboard;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.AbstractWidgetRenderer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -44,6 +48,15 @@ public class RenderWidgetAction
ActionHelper.validateSession(input);
AbstractWidgetRenderer widgetRenderer = QCodeLoader.getAdHoc(AbstractWidgetRenderer.class, input.getWidgetMetaData().getCodeReference());
///////////////////////////////////////////////////////////////
// move default values from meta data into this render input //
///////////////////////////////////////////////////////////////
for(Map.Entry<String, Serializable> entry : input.getWidgetMetaData().getDefaultValues().entrySet())
{
input.addQueryParam(entry.getKey(), ValueUtils.getValueAsString(entry.getValue()));
}
return (widgetRenderer.render(input));
}
}

View File

@ -0,0 +1,164 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.actions.values.SearchPossibleValueSourceAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceOutput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.WidgetDropdownData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Base class for rendering qqq dashboard widgets
**
*******************************************************************************/
public abstract class AbstractWidgetRenderer
{
public static final QValueFormatter valueFormatter = new QValueFormatter();
public static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault());
/*******************************************************************************
**
*******************************************************************************/
public abstract RenderWidgetOutput render(RenderWidgetInput input) throws QException;
/*******************************************************************************
**
*******************************************************************************/
protected boolean setupDropdowns(RenderWidgetInput input, QWidgetMetaData metaData, QWidgetData widgetData) throws QException
{
List<List<Map<String, String>>> pvsData = new ArrayList<>();
List<String> pvsLabels = new ArrayList<>();
List<String> pvsNames = new ArrayList<>();
List<String> missingRequiredSelections = new ArrayList<>();
for(WidgetDropdownData dropdownData : CollectionUtils.nonNullList(metaData.getDropdowns()))
{
String possibleValueSourceName = dropdownData.getPossibleValueSourceName();
QPossibleValueSource possibleValueSource = input.getInstance().getPossibleValueSource(possibleValueSourceName);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this looks complicated, but is just look for a label in the dropdown data and if found use it, //
// otherwise look for label in PVS and if found use that, otherwise just use the PVS name //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
String pvsLabel = dropdownData.getLabel() != null ? dropdownData.getLabel() : (possibleValueSource.getLabel() != null ? possibleValueSource.getLabel() : possibleValueSourceName);
pvsLabels.add(pvsLabel);
pvsNames.add(possibleValueSourceName);
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
if(dropdownData.getForeignKeyFieldName() != null)
{
////////////////////////////////////////
// look for an id in the query params //
////////////////////////////////////////
Integer id = null;
if(input.getQueryParams() != null && input.getQueryParams().containsKey("id") && StringUtils.hasContent(input.getQueryParams().get("id")))
{
id = Integer.parseInt(input.getQueryParams().get("id"));
}
if(id != null)
{
pvsInput.setDefaultQueryFilter(new QQueryFilter().withCriteria(
new QFilterCriteria(
dropdownData.getForeignKeyFieldName(),
QCriteriaOperator.EQUALS,
id)));
}
}
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(pvsInput);
List<Map<String, String>> dropdownOptionList = new ArrayList<>();
pvsData.add(dropdownOptionList);
//////////////////////////////////////////
// sort results, dedupe, and add to map //
//////////////////////////////////////////
Set<String> exists = new HashSet<>();
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
for(QPossibleValue<?> possibleValue : output.getResults())
{
dropdownOptionList.add(Map.of(
"id", String.valueOf(possibleValue.getId()),
"label", possibleValue.getLabel()
));
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// because we know the dropdowns and what the field names will be when something is selected, we can make //
// sure that something has been selected, and if not, display a message that a selection needs made //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(dropdownData.getIsRequired())
{
if(!input.getQueryParams().containsKey(possibleValueSourceName) || !StringUtils.hasContent(input.getQueryParams().get(possibleValueSourceName)))
{
missingRequiredSelections.add(pvsLabel);
}
}
}
widgetData.setDropdownNameList(pvsNames);
widgetData.setDropdownLabelList(pvsLabels);
widgetData.setDropdownDataList(pvsData);
////////////////////////////////////////////////////////////////////////////////
// if there are any missing required dropdowns, build up a message to display //
////////////////////////////////////////////////////////////////////////////////
if(missingRequiredSelections.size() > 0)
{
StringBuilder sb = new StringBuilder("Please select a ").append(StringUtils.joinWithCommasAndAnd(missingRequiredSelections));
sb.append(" from the ").append(StringUtils.plural(missingRequiredSelections.size(), "dropdown", "dropdowns")).append(" above.");
widgetData.setDropdownNeedsSelectedText(sb.toString());
return (false);
}
else
{
return (true);
}
}
}

View File

@ -0,0 +1,258 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ChildRecordListData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.AbstractWidgetMetaDataBuilder;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
/*******************************************************************************
** Generic widget for display a list of child records.
*******************************************************************************/
public class ChildRecordListRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
public static Builder widgetMetaDataBuilder(QJoinMetaData join)
{
return (new Builder(new QWidgetMetaData()
.withName(join.getName())
.withIsCard(true)
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withDefaultValue("joinName", join.getName())));
}
/*******************************************************************************
**
*******************************************************************************/
public static class Builder extends AbstractWidgetMetaDataBuilder
{
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public Builder(QWidgetMetaData widgetMetaData)
{
super(widgetMetaData);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withName(String name)
{
widgetMetaData.setName(name);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withLabel(String label)
{
widgetMetaData.setLabel(label);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withMaxRows(Integer maxRows)
{
widgetMetaData.withDefaultValue("maxRows", maxRows);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withCanAddChildRecord(boolean b)
{
widgetMetaData.withDefaultValue("canAddChildRecord", true);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public Builder withDisabledFieldsForNewChildRecords(Set<String> disabledFieldsForNewChildRecords)
{
widgetMetaData.withDefaultValue("disabledFieldsForNewChildRecords", new HashSet<>(disabledFieldsForNewChildRecords));
return (this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
String widgetLabel = input.getQueryParams().get("widgetLabel");
String joinName = input.getQueryParams().get("joinName");
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
QTableMetaData leftTable = input.getInstance().getTable(join.getLeftTable());
QTableMetaData rightTable = input.getInstance().getTable(join.getRightTable());
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
else if(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"))
{
maxRows = ValueUtils.getValueAsInteger(input.getWidgetMetaData().getDefaultValues().containsKey("maxRows"));
}
////////////////////////////////////////////////////////
// fetch the record that we're getting children for. //
// e.g., the left-side of the join, with the input id //
////////////////////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord record = getOutput.getRecord();
if(record == null)
{
throw (new QNotFoundException("Could not find " + (leftTable == null ? "" : leftTable.getLabel()) + " with primary key " + id));
}
////////////////////////////////////////////////////////////////////
// set up the query - for the table on the right side of the join //
////////////////////////////////////////////////////////////////////
QQueryFilter filter = new QQueryFilter();
for(JoinOn joinOn : join.getJoinOns())
{
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, List.of(record.getValue(joinOn.getLeftField()))));
}
filter.setOrderBys(join.getOrderBys());
filter.setLimit(maxRows);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QValueFormatter.setBlobValuesToDownloadUrls(rightTable, queryOutput.getRecords());
int totalRows = queryOutput.getRecords().size();
if(maxRows != null && (queryOutput.getRecords().size() == maxRows))
{
/////////////////////////////////////////////////////////////////////////////////////
// if the input said to only do some max, and the # of results we got is that max, //
// then do a count query, for displaying 1-n of <count> //
/////////////////////////////////////////////////////////////////////////////////////
CountInput countInput = new CountInput();
countInput.setTableName(join.getRightTable());
countInput.setFilter(filter);
totalRows = new CountAction().execute(countInput).getCount();
}
String tablePath = input.getInstance().getTablePath(rightTable.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, rightTable, tablePath, viewAllLink, totalRows);
if(BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(input.getQueryParams().get("canAddChildRecord"))))
{
widgetData.setCanAddChildRecord(true);
//////////////////////////////////////////////////////////
// new child records must have values from the join-ons //
//////////////////////////////////////////////////////////
Map<String, Serializable> defaultValuesForNewChildRecords = new HashMap<>();
for(JoinOn joinOn : join.getJoinOns())
{
defaultValuesForNewChildRecords.put(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
widgetData.setDefaultValuesForNewChildRecords(defaultValuesForNewChildRecords);
Map<String, Serializable> widgetValues = input.getWidgetMetaData().getDefaultValues();
if(widgetValues.containsKey("disabledFieldsForNewChildRecords"))
{
widgetData.setDisabledFieldsForNewChildRecords((Set<String>) widgetValues.get("disabledFieldsForNewChildRecords"));
}
}
return (new RenderWidgetOutput(widgetData));
}
}

View File

@ -0,0 +1,249 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAdjusters;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Enum to define various "levels" of group-by for on dashboards that want to
** group records by, e.g., year, or month, or week, or day, or hour.
*******************************************************************************/
public enum DateTimeGroupBy
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - double %'s on the time format strings here, because this is a java-format string, which will get //
// its '%s' replaced with a column name, and so then those %'s for the date_format need escaped as %%. //
// See https://www.w3schools.com/sql/func_mysql_date_format.asp for DATE_FORMAT args //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
YEAR("%%Y", MillisPer.YEAR, 1, ChronoUnit.YEARS, DateTimeFormatter.ofPattern("yyyy"), DateTimeFormatter.ofPattern("yyyy")),
MONTH("%%Y-%%m", 2 * MillisPer.MONTH, 1, ChronoUnit.MONTHS, DateTimeFormatter.ofPattern("yyyy-MM"), DateTimeFormatter.ofPattern("MMM'.' yyyy")),
WEEK("%%XW%%V", 35 * MillisPer.DAY, 7, ChronoUnit.DAYS, DateTimeFormatter.ofPattern("YYYY'W'ww"), DateTimeFormatter.ofPattern("YYYY'W'w")),
DAY("%%Y-%%m-%%d", 36 * MillisPer.HOUR, 1, ChronoUnit.DAYS, DateTimeFormatter.ofPattern("yyyy-MM-dd"), DateTimeFormatter.ofPattern("EEE'.' M'/'d")),
HOUR("%%Y-%%m-%%dT%%H", 0, 1, ChronoUnit.HOURS, DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH"), DateTimeFormatter.ofPattern("h a"));
/*******************************************************************************
**
*******************************************************************************/
public interface MillisPer
{
long HOUR = 60 * 60 * 1000;
long DAY = 24 * HOUR;
long WEEK = 7 * DAY;
long MONTH = 30 * DAY;
long YEAR = 365 * DAY;
}
private final String sqlDateFormat;
private final long millisThreshold;
private final int noOfChronoUnitsToAdd;
private final ChronoUnit chronoUnitToAdd;
private final DateTimeFormatter selectedStringFormatter;
private final DateTimeFormatter humanStringFormatter;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
DateTimeGroupBy(String sqlDateFormat, long millisThreshold, int noOfChronoUnitsToAdd, ChronoUnit chronoUnitToAdd, DateTimeFormatter selectedStringFormatter, DateTimeFormatter humanStringFormatter)
{
this.sqlDateFormat = sqlDateFormat;
this.millisThreshold = millisThreshold;
this.noOfChronoUnitsToAdd = noOfChronoUnitsToAdd;
this.chronoUnitToAdd = chronoUnitToAdd;
this.selectedStringFormatter = selectedStringFormatter;
this.humanStringFormatter = humanStringFormatter;
}
/*******************************************************************************
**
*******************************************************************************/
public String getSqlExpression()
{
ZoneId sessionOrInstanceZoneId = ValueUtils.getSessionOrInstanceZoneId();
String targetTimezone = sessionOrInstanceZoneId.toString();
if("Z".equals(targetTimezone) || !StringUtils.hasContent(targetTimezone))
{
targetTimezone = "UTC";
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
// if we only had a timezone offset (not a zone name/id), then the zoneId's toString will look like //
// UTC-05:00. MySQL doesn't want that, so, strip away the leading UTC, to just get -05:00 //
//////////////////////////////////////////////////////////////////////////////////////////////////////
if((targetTimezone.startsWith("UTC-") || targetTimezone.startsWith("UTC+")) && targetTimezone.length() > 5)
{
targetTimezone = targetTimezone.substring(3);
}
return "DATE_FORMAT(CONVERT_TZ(%s, 'UTC', '" + targetTimezone + "'), '" + sqlDateFormat + "')";
/*
if(this == WEEK)
{
return "YEARWEEK(CONVERT_TZ(%s, 'UTC', '" + targetTimezone + "'), 6)";
}
else
{
return "DATE_FORMAT(CONVERT_TZ(%s, 'UTC', '" + targetTimezone + "'), '" + sqlDateFormat + "')";
}
*/
}
/*******************************************************************************
** get an instance of this enum, based on start & end instants - look at the #
** of millis between them, and return the first enum value w/ a millisThreshold
** under that difference. Default to HOUR.
*******************************************************************************/
public static DateTimeGroupBy selectFromStartAndEndTimes(Instant start, Instant end)
{
long millisBetween = end.toEpochMilli() - start.toEpochMilli();
for(DateTimeGroupBy value : DateTimeGroupBy.values())
{
if(millisBetween > value.millisThreshold)
{
return (value);
}
}
return (HOUR);
}
/*******************************************************************************
** Make an Instant into a string that will match what came out of the database's
** DATE_FORMAT() function
*******************************************************************************/
public String makeSelectedString(Instant time)
{
ZonedDateTime zoned = time.atZone(ValueUtils.getSessionOrInstanceZoneId());
if(this == WEEK)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// so, it seems like database is returning, e.g., W00-W52, but java is doing W1-W53... //
// which, apparently we can compensate for by adding a week? not sure, but results seemed right. //
////////////////////////////////////////////////////////////////////////////////////////////////////
zoned = zoned.plusDays(7);
int weekYear = zoned.get(IsoFields.WEEK_BASED_YEAR);
int week = zoned.get(IsoFields.WEEK_OF_WEEK_BASED_YEAR);
return (String.format("%04dW%02d", weekYear, week));
}
return (selectedStringFormatter.format(zoned));
}
/*******************************************************************************
** Make a string to show to a user
*******************************************************************************/
public String makeHumanString(Instant instant)
{
ZonedDateTime zoned = instant.atZone(ValueUtils.getSessionOrInstanceZoneId());
if(this.equals(WEEK))
{
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("M'/'d");
while(zoned.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
////////////////////////////////////////
// go backwards until sunday is found //
////////////////////////////////////////
zoned = zoned.minus(1, ChronoUnit.DAYS);
}
return (dateTimeFormatter.format(zoned) + "-" + dateTimeFormatter.format(zoned.plusDays(6)));
/*
int weekOfYear = zoned.get(ChronoField.ALIGNED_WEEK_OF_YEAR);
ZonedDateTime sunday = zoned.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, weekOfYear).with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY));
ZonedDateTime saturday = sunday.with(TemporalAdjusters.next(DayOfWeek.SATURDAY));
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("M'/'d");
return (dateTimeFormatter.format(sunday) + "-" + dateTimeFormatter.format(saturday));
*/
}
return (humanStringFormatter.format(zoned));
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
public Instant roundDown(Instant instant)
{
ZonedDateTime zoned = instant.atZone(ValueUtils.getSessionOrInstanceZoneId());
return switch(this)
{
case YEAR -> zoned.with(TemporalAdjusters.firstDayOfYear()).truncatedTo(ChronoUnit.DAYS).toInstant();
case MONTH -> zoned.with(TemporalAdjusters.firstDayOfMonth()).truncatedTo(ChronoUnit.DAYS).toInstant();
case WEEK ->
{
while(zoned.get(ChronoField.DAY_OF_WEEK) != DayOfWeek.SUNDAY.getValue())
{
zoned = zoned.minusDays(1);
}
yield (zoned.truncatedTo(ChronoUnit.DAYS).toInstant());
}
case DAY -> zoned.truncatedTo(ChronoUnit.DAYS).toInstant();
case HOUR -> zoned.truncatedTo(ChronoUnit.HOURS).toInstant();
};
}
/*******************************************************************************
**
*******************************************************************************/
public Instant increment(Instant instant)
{
ZonedDateTime zoned = instant.atZone(ValueUtils.getSessionOrInstanceZoneId());
return (zoned.plus(noOfChronoUnitsToAdd, chronoUnitToAdd).toInstant());
}
}

View File

@ -0,0 +1,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
/*******************************************************************************
**
*******************************************************************************/
public class DefaultWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return new RenderWidgetOutput(new DefaultWidgetData(input));
}
/*******************************************************************************
**
*******************************************************************************/
public static class DefaultWidgetData extends QWidgetData
{
private final String type;
private final Map<String, String> queryParams;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DefaultWidgetData(RenderWidgetInput renderWidgetInput)
{
this.type = renderWidgetInput.getWidgetMetaData().getType();
this.queryParams = renderWidgetInput.getQueryParams();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getType()
{
return (type);
}
/*******************************************************************************
** Getter for queryParams
**
*******************************************************************************/
public Map<String, String> getQueryParams()
{
return queryParams;
}
}
}

View File

@ -19,32 +19,29 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard;
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.DividerWidgetData;
/*******************************************************************************
** Base class for rendering qqq dashboard widgets
**
** Generic widget for showing a divider
*******************************************************************************/
public abstract class AbstractWidgetRenderer
public class DividerWidgetRenderer extends AbstractWidgetRenderer
{
public static final QValueFormatter valueFormatter = new QValueFormatter();
public static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mma").withZone(ZoneId.systemDefault());
public static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault());
/*******************************************************************************
**
*******************************************************************************/
public abstract RenderWidgetOutput render(RenderWidgetInput input) throws QException;
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
return (new RenderWidgetOutput(new DividerWidgetData()));
}
}

View File

@ -0,0 +1,153 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
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.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.RawHTML;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.AbstractWidgetValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.QNoCodeWidgetMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class NoCodeWidgetRenderer extends AbstractWidgetRenderer
{
private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetRenderer.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
QNoCodeWidgetMetaData widgetMetaData = (QNoCodeWidgetMetaData) input.getWidgetMetaData();
Map<String, Object> context = initContext(input);
context.putAll(input.getQueryParams());
///////////////////////////////////////////////
// populate context by evaluating all values //
///////////////////////////////////////////////
for(AbstractWidgetValueSource valueSource : widgetMetaData.getValues())
{
try
{
LOG.trace("Computing: " + valueSource.getType() + " named " + valueSource.getName() + "...");
Object value = valueSource.evaluate(context, input);
LOG.trace("Computed: " + valueSource.getName() + " = " + value);
context.put(valueSource.getName(), value);
context.put(valueSource.getName() + ".source", valueSource);
}
catch(Exception e)
{
LOG.warn("Error evaluating widget value source", e, logPair("widgetName", input.getWidgetMetaData().getName()), logPair("valueSourceName", valueSource.getName()));
}
}
/////////////////////////////////////////////
// build content by evaluating all outputs //
/////////////////////////////////////////////
List<AbstractWidgetOutput> outputs = widgetMetaData.getOutputs();
String content = renderOutputs(context, outputs);
return (new RenderWidgetOutput(new RawHTML(widgetMetaData.getLabel(), content)));
}
/*******************************************************************************
**
*******************************************************************************/
public Map<String, Object> initContext(RenderWidgetInput input)
{
Map<String, Object> context = new HashMap<>();
context.put("utils", new NoCodeWidgetVelocityUtils(context, input));
context.put("input", input);
return context;
}
/*******************************************************************************
**
*******************************************************************************/
public String renderOutputs(Map<String, Object> context, List<AbstractWidgetOutput> outputs) throws QException
{
StringBuilder content = new StringBuilder();
for(AbstractWidgetOutput output : CollectionUtils.nonNullList(outputs))
{
boolean conditionPassed = true;
if(output.getCondition() != null)
{
conditionPassed = evaluateCondition(output.getCondition(), context);
}
if(conditionPassed)
{
String render = output.render(context);
content.append(render);
LOG.trace("Condition passed, rendered: " + render);
}
else
{
LOG.trace("Condition failed - not rendering this output.");
}
}
return (content.toString());
}
/*******************************************************************************
**
*******************************************************************************/
private boolean evaluateCondition(QFilterCriteria condition, Map<String, Object> context)
{
try
{
Object value = context.get(condition.getFieldName());
return (BackendQueryFilterUtils.doesCriteriaMatch(condition, condition.getFieldName(), (Serializable) value));
}
catch(Exception e)
{
LOG.warn("Error evaluating condition: " + condition, e);
return (false);
}
}
}

View File

@ -0,0 +1,341 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.dashboard.AbstractHTMLWidgetRenderer;
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.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode.WidgetCount;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
**
*******************************************************************************/
public class NoCodeWidgetVelocityUtils
{
private static final QLogger LOG = QLogger.getLogger(NoCodeWidgetVelocityUtils.class);
private Map<String, Object> context;
private RenderWidgetInput input;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public NoCodeWidgetVelocityUtils(Map<String, Object> context, RenderWidgetInput input)
{
this.context = context;
this.input = input;
}
/*******************************************************************************
**
*******************************************************************************/
public String icon(String iconName, String color)
{
return String.format("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: %s; position: relative; top: 6px;" aria-hidden="true">%s</span>
""", color, iconName);
}
/*******************************************************************************
**
*******************************************************************************/
public String helpIcon()
{
return (icon("help_outline", "blue"));
}
/*******************************************************************************
**
*******************************************************************************/
public String errorIcon()
{
return (icon("error_outline", "red"));
}
/*******************************************************************************
**
*******************************************************************************/
public String warningIcon()
{
return (icon("warning", "orange"));
}
/*******************************************************************************
**
*******************************************************************************/
public String checkIcon()
{
return (icon("check", "green"));
}
/*******************************************************************************
**
*******************************************************************************/
public String pendingIcon()
{
return (icon("pending", "#0062ff"));
}
/*******************************************************************************
**
*******************************************************************************/
public String spanColorGreen()
{
return ("""
<span style="color: green;">
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String spanColorOrange()
{
return ("""
<span style="color: orange;">
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String spanColorRed()
{
return ("""
<span style="color: red;">
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String plural(Integer size, String ifOne, String ifNotOne)
{
return StringUtils.plural(size, ifOne, ifNotOne);
}
/*******************************************************************************
**
*******************************************************************************/
public String formatDateTime(Instant i)
{
if(i == null)
{
return ("");
}
return QValueFormatter.formatDateTimeWithZone(i.atZone(ZoneId.of(QContext.getQInstance().getDefaultTimeZoneId())));
}
/*******************************************************************************
**
*******************************************************************************/
public String formatSecondsAsDuration(Integer seconds)
{
StringBuilder rs = new StringBuilder();
if(seconds == null)
{
return ("");
}
int secondsPerDay = 24 * 60 * 60;
if(seconds >= secondsPerDay)
{
int days = seconds / (secondsPerDay);
seconds = seconds % secondsPerDay;
rs.append(days).append(StringUtils.plural(days, " day", " days")).append(" ");
}
int secondsPerHour = 60 * 60;
if(seconds >= secondsPerHour)
{
int hours = seconds / (secondsPerHour);
seconds = seconds % secondsPerHour;
rs.append(hours).append(StringUtils.plural(hours, " hour", " hours")).append(" ");
}
int secondsPerMinute = 60;
if(seconds >= secondsPerMinute)
{
int minutes = seconds / (secondsPerMinute);
seconds = seconds % secondsPerMinute;
rs.append(minutes).append(StringUtils.plural(minutes, " minute", " minutes")).append(" ");
}
if(seconds > 0 || rs.length() == 0)
{
rs.append(seconds).append(StringUtils.plural(seconds, " second", " seconds")).append(" ");
}
if(rs.length() > 0)
{
rs.deleteCharAt(rs.length() - 1);
}
return (rs.toString());
}
/*******************************************************************************
**
*******************************************************************************/
public String formatSecondsAsRoundedDuration(Integer seconds)
{
StringBuilder rs = new StringBuilder();
if(seconds == null)
{
return ("");
}
int secondsPerDay = 24 * 60 * 60;
if(seconds >= secondsPerDay)
{
int days = seconds / (secondsPerDay);
return (days + StringUtils.plural(days, " day", " days"));
}
int secondsPerHour = 60 * 60;
if(seconds >= secondsPerHour)
{
int hours = seconds / (secondsPerHour);
return (hours + StringUtils.plural(hours, " hour", " hours"));
}
int secondsPerMinute = 60;
if(seconds >= secondsPerMinute)
{
int minutes = seconds / (secondsPerMinute);
return (minutes + StringUtils.plural(minutes, " minute", " minutes"));
}
if(seconds > 0 || rs.length() == 0)
{
return (seconds + StringUtils.plural(seconds, " second", " seconds"));
}
return ("");
}
/*******************************************************************************
**
*******************************************************************************/
public String tableCountFilterLink(String countVariableName, String singular, String plural) throws QException
{
try
{
WidgetCount widgetCount = (WidgetCount) context.get(countVariableName + ".source");
Integer count = ValueUtils.getValueAsInteger(context.get(countVariableName));
QQueryFilter filter = widgetCount.getEffectiveFilter(input);
return (AbstractHTMLWidgetRenderer.aHrefTableFilterNoOfRecords(widgetCount.getTableName(), filter, count, singular, plural));
}
catch(Exception e)
{
LOG.warn("Error rendering widget link", e);
return ("");
}
}
/*******************************************************************************
**
*******************************************************************************/
public String format(String displayFormat, Serializable value)
{
return (QValueFormatter.formatValue(displayFormat, value));
}
/*******************************************************************************
**
*******************************************************************************/
public String round(BigDecimal input, int digits)
{
return String.valueOf(input.setScale(digits, RoundingMode.HALF_UP));
}
/*******************************************************************************
**
*******************************************************************************/
public Object ifElse(Object ifObject, Object elseObject)
{
if(StringUtils.hasContent(ValueUtils.getValueAsString(ifObject)))
{
return (ifObject);
}
else if(StringUtils.hasContent(ValueUtils.getValueAsString(elseObject)))
{
return (elseObject);
}
return ("");
}
}

View File

@ -0,0 +1,71 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ParentWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.ParentWidgetMetaData;
/*******************************************************************************
** Generic widget for display a parent widget with children of possible values,
** child widgets, and child actions
*******************************************************************************/
public class ParentWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
try
{
ParentWidgetMetaData metaData = (ParentWidgetMetaData) input.getWidgetMetaData();
ParentWidgetData widgetData = new ParentWidgetData();
/////////////////////////////////////////////////////////////
// handle any PVSs creating dropdown data for the frontend //
/////////////////////////////////////////////////////////////
boolean dropdownsValid = setupDropdowns(input, metaData, widgetData);
if(dropdownsValid)
{
widgetData.setChildWidgetNameList(metaData.getChildWidgetNameList());
}
return (new RenderWidgetOutput(widgetData));
}
catch(Exception e)
{
throw (new QException("Error rendering parent widget", e));
}
}
}

View File

@ -0,0 +1,73 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.ProcessWidgetData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
/*******************************************************************************
** Generic widget for displaying a process as a widget
*******************************************************************************/
public class ProcessWidgetRenderer extends AbstractWidgetRenderer
{
public static final String WIDGET_PROCESS_NAME = "processName";
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
ActionHelper.validateSession(input);
try
{
ProcessWidgetData data = new ProcessWidgetData();
if(input.getWidgetMetaData() instanceof QWidgetMetaData widgetMetaData)
{
setupDropdowns(input, widgetMetaData, data);
String processName = (String) widgetMetaData.getDefaultValues().get(WIDGET_PROCESS_NAME);
QProcessMetaData processMetaData = input.getInstance().getProcess(processName);
data.setProcessMetaData(processMetaData);
data.setDefaultValues(new HashMap<>(input.getQueryParams()));
}
return (new RenderWidgetOutput(data));
}
catch(Exception e)
{
throw (new QException("Error rendering process widget", e));
}
}
}

View File

@ -19,14 +19,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard;
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidget;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QuickSightChart;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QuickSightChartMetaData;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
@ -74,8 +74,8 @@ public class QuickSightChartRenderer extends AbstractWidgetRenderer
final GenerateEmbedUrlForRegisteredUserResponse generateEmbedUrlForRegisteredUserResponse = quickSightClient.generateEmbedUrlForRegisteredUser(generateEmbedUrlForRegisteredUserRequest);
String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl();
QWidget widget = new QuickSightChart(input.getWidgetMetaData().getName(), quickSightMetaData.getLabel(), embedUrl);
String embedUrl = generateEmbedUrlForRegisteredUserResponse.embedUrl();
QWidgetData widget = new QuickSightChart(input.getWidgetMetaData().getName(), quickSightMetaData.getLabel(), embedUrl);
return (new RenderWidgetOutput(widget));
}
catch(Exception e)

View File

@ -0,0 +1,62 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.math.BigDecimal;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.USMapWidgetData;
/*******************************************************************************
** Generic widget for display a map of the us
*******************************************************************************/
public class USMapWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return (new RenderWidgetOutput(
new USMapWidgetData()
.withHeight("250px")
.withMapMarkerList(generateMapMarkerList())
));
}
/*******************************************************************************
**
*******************************************************************************/
protected List<USMapWidgetData.MapMarker> generateMapMarkerList() throws QException
{
return (List.of(new USMapWidgetData.MapMarker("maryville", new BigDecimal("38.725278"), new BigDecimal("-89.957778"))));
}
}

View File

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

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

@ -50,4 +50,13 @@ public interface DeleteInterface
return (false);
}
/*******************************************************************************
** Specify whether this particular module's delete action can & should fetch
** records before deleting them, e.g., for audits or "not-found-checks"
*******************************************************************************/
default boolean supportsPreFetchQuery()
{
return (true);
}
}

View File

@ -22,9 +22,12 @@
package com.kingsrook.qqq.backend.core.actions.interfaces;
import java.util.HashSet;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
/*******************************************************************************
@ -37,4 +40,34 @@ public interface GetInterface
**
*******************************************************************************/
GetOutput execute(GetInput getInput) throws QException;
/*******************************************************************************
**
*******************************************************************************/
default void validateInput(GetInput getInput) throws QException
{
if(getInput.getPrimaryKey() != null & getInput.getUniqueKey() != null)
{
throw new QException("A GetInput may not contain both a primary key [" + getInput.getPrimaryKey() + "] and unique key [" + getInput.getUniqueKey() + "]");
}
if(getInput.getUniqueKey() != null)
{
QTableMetaData table = getInput.getTable();
boolean foundMatch = false;
for(UniqueKey uniqueKey : table.getUniqueKeys())
{
if(new HashSet<>(uniqueKey.getFieldNames()).equals(getInput.getUniqueKey().keySet()))
{
foundMatch = true;
break;
}
}
if(!foundMatch)
{
throw new QException("Table [" + table.getName() + "] does not have a unique key defined on fields: " + getInput.getUniqueKey().keySet().stream().sorted().toList());
}
}
}
}

View File

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

@ -37,4 +37,14 @@ public interface UpdateInterface
**
*******************************************************************************/
UpdateOutput execute(UpdateInput updateInput) throws QException;
/*******************************************************************************
** Specify whether this particular module's update action can & should fetch
** records before updating them, e.g., for audits or "not-found-checks"
*******************************************************************************/
default boolean supportsPreFetchQuery()
{
return (true);
}
}

View File

@ -0,0 +1,336 @@
/*
* 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.metadata;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** Object to represent the graph of joins in a QQQ Instance. e.g., all of the
** connections among tables through joins.
*******************************************************************************/
public class JoinGraph
{
private Set<Edge> edges = new HashSet<>();
/*******************************************************************************
** Graph edge (no graph nodes needed in here)
*******************************************************************************/
private record Edge(String joinName, String leftTable, String rightTable)
{
}
/*******************************************************************************
** In this class, we are treating joins as non-directional graph edges - so -
** use this class to "normalize" what may otherwise be duplicated joins in the
** qInstance (e.g., A -> B and B -> A -- in the instance, those are valid, but
** in our graph here, we want to consider those the same).
*******************************************************************************/
private static class NormalizedJoin
{
private String tableA;
private String tableB;
private String joinFieldA;
private String joinFieldB;
/*******************************************************************************
**
*******************************************************************************/
public NormalizedJoin(QJoinMetaData joinMetaData)
{
boolean needFlip = false;
int tableCompare = joinMetaData.getLeftTable().compareTo(joinMetaData.getRightTable());
if(tableCompare < 0)
{
needFlip = true;
}
else if(tableCompare == 0)
{
int fieldCompare = joinMetaData.getJoinOns().get(0).getLeftField().compareTo(joinMetaData.getJoinOns().get(0).getRightField());
if(fieldCompare < 0)
{
needFlip = true;
}
}
if(needFlip)
{
joinMetaData = joinMetaData.flip();
}
tableA = joinMetaData.getLeftTable();
tableB = joinMetaData.getRightTable();
joinFieldA = joinMetaData.getJoinOns().get(0).getLeftField();
joinFieldB = joinMetaData.getJoinOns().get(0).getRightField();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
NormalizedJoin that = (NormalizedJoin) o;
return Objects.equals(tableA, that.tableA) && Objects.equals(tableB, that.tableB) && Objects.equals(joinFieldA, that.joinFieldA) && Objects.equals(joinFieldB, that.joinFieldB);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int hashCode()
{
return Objects.hash(tableA, tableB, joinFieldA, joinFieldB);
}
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public JoinGraph(QInstance qInstance)
{
Set<NormalizedJoin> usedJoins = new HashSet<>();
for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values())
{
NormalizedJoin normalizedJoin = new NormalizedJoin(join);
if(usedJoins.contains(normalizedJoin))
{
continue;
}
usedJoins.add(normalizedJoin);
edges.add(new Edge(join.getName(), join.getLeftTable(), join.getRightTable()));
}
}
/*******************************************************************************
**
*******************************************************************************/
public record JoinConnection(String joinTable, String viaJoinName) implements Comparable<JoinConnection>
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public int compareTo(JoinConnection that)
{
Comparator<JoinConnection> comparator = Comparator.comparing((JoinConnection jc) -> jc.joinTable())
.thenComparing((JoinConnection jc) -> jc.viaJoinName());
return (comparator.compare(this, that));
}
}
/*******************************************************************************
**
*******************************************************************************/
public record JoinConnectionList(List<JoinConnection> list) implements Comparable<JoinConnectionList>
{
/*******************************************************************************
**
*******************************************************************************/
public JoinConnectionList copy()
{
return new JoinConnectionList(new ArrayList<>(list));
}
/*******************************************************************************
**
*******************************************************************************/
public int compareTo(JoinConnectionList that)
{
if(this.equals(that))
{
return (0);
}
for(int i = 0; i < Math.min(this.list.size(), that.list.size()); i++)
{
int comp = this.list.get(i).compareTo(that.list.get(i));
if(comp != 0)
{
return (comp);
}
}
return (this.list.size() - that.list.size());
}
/*******************************************************************************
**
*******************************************************************************/
public boolean matchesJoinPath(List<String> joinPath)
{
if(list.size() != joinPath.size())
{
return (false);
}
for(int i = 0; i < list.size(); i++)
{
if(!list.get(i).viaJoinName().equals(joinPath.get(i)))
{
return (false);
}
}
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
public String getJoinNamesAsString()
{
return (StringUtils.join(", ", list().stream().map(jc -> jc.viaJoinName()).toList()));
}
/*******************************************************************************
**
*******************************************************************************/
public List<String> getJoinNamesAsList()
{
return (list().stream().map(jc -> jc.viaJoinName()).toList());
}
}
/*******************************************************************************
**
*******************************************************************************/
public Set<JoinConnectionList> getJoinConnections(String tableName)
{
Set<JoinConnectionList> rs = new TreeSet<>();
doGetJoinConnections(rs, tableName, new ArrayList<>(), new JoinConnectionList(new ArrayList<>()));
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private void doGetJoinConnections(Set<JoinConnectionList> joinConnections, String tableName, List<String> path, JoinConnectionList connectionList)
{
for(Edge edge : edges)
{
if(edge.leftTable.equals(tableName) || edge.rightTable.equals(tableName))
{
if(path.contains(edge.joinName))
{
continue;
}
List<String> newPath = new ArrayList<>(path);
newPath.add(edge.joinName);
if(!joinConnectionsContain(joinConnections, newPath))
{
String otherTableName = null;
if(!edge.leftTable.equals(tableName))
{
otherTableName = edge.leftTable;
}
else if(!edge.rightTable.equals(tableName))
{
otherTableName = edge.rightTable;
}
if(otherTableName != null)
{
JoinConnectionList newConnectionList = connectionList.copy();
JoinConnection joinConnection = new JoinConnection(otherTableName, edge.joinName);
newConnectionList.list.add(joinConnection);
joinConnections.add(newConnectionList);
doGetJoinConnections(joinConnections, otherTableName, new ArrayList<>(newPath), newConnectionList);
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private boolean joinConnectionsContain(Set<JoinConnectionList> joinPaths, List<String> newPath)
{
for(JoinConnectionList joinConnections : joinPaths)
{
List<String> joinConnectionJoins = joinConnections.list.stream().map(jc -> jc.viaJoinName).toList();
if(joinConnectionJoins.equals(newPath))
{
return (true);
}
}
return (false);
}
}

View File

@ -27,6 +27,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
@ -40,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMeta
import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@ -70,21 +73,41 @@ public class MetaDataAction
Map<String, QFrontendTableMetaData> tables = new LinkedHashMap<>();
for(Map.Entry<String, QTableMetaData> entry : metaDataInput.getInstance().getTables().entrySet())
{
String tableName = entry.getKey();
String tableName = entry.getKey();
QTableMetaData table = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName);
tables.put(tableName, new QFrontendTableMetaData(backendForTable, entry.getValue(), false));
treeNodes.put(tableName, new AppTreeNode(entry.getValue()));
tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false));
treeNodes.put(tableName, new AppTreeNode(table));
}
metaDataOutput.setTables(tables);
// addJoinsToTables(tables);
// addJoinedTablesToTables(tables);
////////////////////////////////////////
// map processes to frontend metadata //
////////////////////////////////////////
Map<String, QFrontendProcessMetaData> processes = new LinkedHashMap<>();
for(Map.Entry<String, QProcessMetaData> entry : metaDataInput.getInstance().getProcesses().entrySet())
{
processes.put(entry.getKey(), new QFrontendProcessMetaData(entry.getValue(), false));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String processName = entry.getKey();
QProcessMetaData process = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
processes.put(processName, new QFrontendProcessMetaData(metaDataInput, process, false));
treeNodes.put(processName, new AppTreeNode(process));
}
metaDataOutput.setProcesses(processes);
@ -94,8 +117,17 @@ public class MetaDataAction
Map<String, QFrontendReportMetaData> reports = new LinkedHashMap<>();
for(Map.Entry<String, QReportMetaData> entry : metaDataInput.getInstance().getReports().entrySet())
{
reports.put(entry.getKey(), new QFrontendReportMetaData(entry.getValue(), false));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String reportName = entry.getKey();
QReportMetaData report = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
reports.put(reportName, new QFrontendReportMetaData(metaDataInput, report, false));
treeNodes.put(reportName, new AppTreeNode(report));
}
metaDataOutput.setReports(reports);
@ -105,7 +137,16 @@ public class MetaDataAction
Map<String, QFrontendWidgetMetaData> widgets = new LinkedHashMap<>();
for(Map.Entry<String, QWidgetMetaDataInterface> entry : metaDataInput.getInstance().getWidgets().entrySet())
{
widgets.put(entry.getKey(), new QFrontendWidgetMetaData(entry.getValue()));
String widgetName = entry.getKey();
QWidgetMetaDataInterface widget = entry.getValue();
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
widgets.put(widgetName, new QFrontendWidgetMetaData(metaDataInput, widget));
}
metaDataOutput.setWidgets(widgets);
@ -115,14 +156,32 @@ public class MetaDataAction
Map<String, QFrontendAppMetaData> apps = new LinkedHashMap<>();
for(Map.Entry<String, QAppMetaData> entry : metaDataInput.getInstance().getApps().entrySet())
{
apps.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue()));
treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue()));
String appName = entry.getKey();
QAppMetaData app = entry.getValue();
if(CollectionUtils.nullSafeHasContents(entry.getValue().getChildren()))
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, app);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
for(QAppChildMetaData child : entry.getValue().getChildren())
continue;
}
apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput));
treeNodes.put(appName, new AppTreeNode(app));
if(CollectionUtils.nullSafeHasContents(app.getChildren()))
{
for(QAppChildMetaData child : app.getChildren())
{
apps.get(entry.getKey()).addChild(new AppTreeNode(child));
if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules)
{
PermissionCheckResult childPermissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules);
if(childPermissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
}
apps.get(appName).addChild(new AppTreeNode(child));
}
}
}
@ -136,7 +195,7 @@ public class MetaDataAction
{
if(appMetaData.getParentAppName() == null)
{
buildAppTree(treeNodes, appTree, appMetaData);
buildAppTree(metaDataInput, treeNodes, appTree, appMetaData);
}
}
metaDataOutput.setAppTree(appTree);
@ -149,6 +208,8 @@ public class MetaDataAction
metaDataOutput.setBranding(metaDataInput.getInstance().getBranding());
}
metaDataOutput.setEnvironmentValues(metaDataInput.getInstance().getEnvironmentValues());
// todo post-customization - can do whatever w/ the result if you want?
return metaDataOutput;
@ -159,7 +220,7 @@ public class MetaDataAction
/*******************************************************************************
**
*******************************************************************************/
private void buildAppTree(Map<String, AppTreeNode> treeNodes, List<AppTreeNode> nodeList, QAppChildMetaData childMetaData)
private void buildAppTree(MetaDataInput metaDataInput, Map<String, AppTreeNode> treeNodes, List<AppTreeNode> nodeList, QAppChildMetaData childMetaData)
{
AppTreeNode treeNode = treeNodes.get(childMetaData.getName());
if(treeNode == null)
@ -174,7 +235,16 @@ public class MetaDataAction
{
for(QAppChildMetaData child : app.getChildren())
{
buildAppTree(treeNodes, treeNode.getChildren(), child);
if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules)
{
PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules);
if(permissionResult.equals(PermissionCheckResult.DENY_HIDE))
{
continue;
}
}
buildAppTree(metaDataInput, treeNodes, treeNode.getChildren(), child);
}
}
}

View File

@ -52,7 +52,7 @@ public class ProcessMetaDataAction
{
throw (new QNotFoundException("Process [" + processMetaDataInput.getProcessName() + "] was not found."));
}
processMetaDataOutput.setProcess(new QFrontendProcessMetaData(process, true));
processMetaDataOutput.setProcess(new QFrontendProcessMetaData(processMetaDataInput, process, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -54,7 +54,7 @@ public class TableMetaDataAction
throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found."));
}
QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName());
tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true));
tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true));
// todo post-customization - can do whatever w/ the result if you want

View File

@ -0,0 +1,204 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
/*******************************************************************************
**
*******************************************************************************/
public class AvailablePermission extends QRecordEntity
{
public static final String TABLE_NAME = "availablePermission";
@QField(label = "Permission Name")
private String name;
@QField(label = "Object")
private String objectName;
@QField()
private String objectType;
@QField()
private String permissionType;
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean equals(Object o)
{
if(this == o)
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
AvailablePermission that = (AvailablePermission) o;
return Objects.equals(name, that.name) && Objects.equals(objectName, that.objectName) && Objects.equals(objectType, that.objectType) && Objects.equals(permissionType, that.permissionType);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public int hashCode()
{
return Objects.hash(name, objectName, objectType, permissionType);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public AvailablePermission withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
** Getter for objectType
*******************************************************************************/
public String getObjectType()
{
return (this.objectType);
}
/*******************************************************************************
** Setter for objectType
*******************************************************************************/
public void setObjectType(String objectType)
{
this.objectType = objectType;
}
/*******************************************************************************
** Fluent setter for objectType
*******************************************************************************/
public AvailablePermission withObjectType(String objectType)
{
this.objectType = objectType;
return (this);
}
/*******************************************************************************
** Getter for permissionType
*******************************************************************************/
public String getPermissionType()
{
return (this.permissionType);
}
/*******************************************************************************
** Setter for permissionType
*******************************************************************************/
public void setPermissionType(String permissionType)
{
this.permissionType = permissionType;
}
/*******************************************************************************
** Fluent setter for permissionType
*******************************************************************************/
public AvailablePermission withPermissionType(String permissionType)
{
this.permissionType = permissionType;
return (this);
}
/*******************************************************************************
** Getter for objectName
*******************************************************************************/
public String getObjectName()
{
return (this.objectName);
}
/*******************************************************************************
** Setter for objectName
*******************************************************************************/
public void setObjectName(String objectName)
{
this.objectName = objectName;
}
/*******************************************************************************
** Fluent setter for objectName
*******************************************************************************/
public AvailablePermission withObjectName(String objectName)
{
this.objectName = objectName;
return (this);
}
}

View File

@ -0,0 +1,67 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
/*******************************************************************************
**
*******************************************************************************/
public class BulkTableActionProcessPermissionChecker implements CustomPermissionChecker
{
private static final QLogger LOG = QLogger.getLogger(BulkTableActionProcessPermissionChecker.class);
/*******************************************************************************
**
*******************************************************************************/
@Override
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
{
String processName = metaDataWithPermissionRules.getName();
if(processName != null && processName.indexOf('.') > -1)
{
String[] parts = processName.split("\\.", 2);
String tableName = parts[0];
String bulkActionName = parts[1];
AbstractTableActionInput tableActionInput = new AbstractTableActionInput();
tableActionInput.setTableName(tableName);
switch(bulkActionName)
{
case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT);
case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT);
case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE);
default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName);
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,602 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithName;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class PermissionsHelper
{
private static final QLogger LOG = QLogger.getLogger(PermissionsHelper.class);
/*******************************************************************************
**
*******************************************************************************/
public static void checkTablePermissionThrowing(AbstractTableActionInput tableActionInput, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
checkTablePermissionThrowing(tableActionInput, tableActionInput.getTableName(), permissionSubType);
}
/*******************************************************************************
**
*******************************************************************************/
private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = QContext.getQInstance().getTable(tableName);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, QContext.getQInstance()), permissionSubType, table.getName());
}
/*******************************************************************************
**
*******************************************************************************/
public static String getTablePermissionName(String tableName, TablePermissionSubType permissionSubType)
{
QInstance qInstance = QContext.getQInstance();
QPermissionRules rules = getEffectivePermissionRules(qInstance.getTable(tableName), qInstance);
String permissionBaseName = getEffectivePermissionBaseName(rules, tableName);
return (getPermissionName(permissionBaseName, permissionSubType));
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasTablePermission(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType)
{
try
{
checkTablePermissionThrowing(actionInput, tableName, permissionSubType);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules)
{
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, QContext.getQInstance());
String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName());
switch(rules.getLevel())
{
case NOT_PROTECTED:
{
/////////////////////////////////////////////////
// if the entity isn't protected, always ALLOW //
/////////////////////////////////////////////////
return PermissionCheckResult.ALLOW;
}
case HAS_ACCESS_PERMISSION:
{
////////////////////////////////////////////////////////////////////////
// if the entity just has a 'has access', then check for 'has access' //
////////////////////////////////////////////////////////////////////////
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
}
case READ_WRITE_PERMISSIONS:
{
////////////////////////////////////////////////////////////////
// if the table is configured w/ read/write, check for either //
////////////////////////////////////////////////////////////////
if(metaDataWithPermissionRules instanceof QTableMetaData)
{
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.READ, PrivatePermissionSubType.WRITE);
}
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS:
{
//////////////////////////////////////////////////////////////////////////
// if the table is configured w/ read/insert/edit/delete, check for any //
//////////////////////////////////////////////////////////////////////////
if(metaDataWithPermissionRules instanceof QTableMetaData)
{
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, TablePermissionSubType.READ, TablePermissionSubType.INSERT, TablePermissionSubType.EDIT, TablePermissionSubType.DELETE);
}
return getPermissionCheckResult(actionInput, rules, permissionBaseName, metaDataWithPermissionRules, PrivatePermissionSubType.HAS_ACCESS);
}
default:
{
return getPermissionDeniedCheckResult(rules);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName) throws QPermissionDeniedException
{
checkProcessPermissionThrowing(actionInput, processName, Collections.emptyMap());
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
{
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, QContext.getQInstance());
if(effectivePermissionRules.getCustomPermissionChecker() != null)
{
/////////////////////////////////////
// todo - avoid stack overflows... //
/////////////////////////////////////
CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, effectivePermissionRules.getCustomPermissionChecker());
customPermissionChecker.checkPermissionsThrowing(actionInput, process);
return;
}
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName());
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasProcessPermission(AbstractActionInput actionInput, String processName)
{
try
{
checkProcessPermissionThrowing(actionInput, processName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{
QAppMetaData app = QContext.getQInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName());
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasAppPermission(AbstractActionInput actionInput, String appName)
{
try
{
checkAppPermissionThrowing(actionInput, appName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{
QReportMetaData report = QContext.getQInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName());
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasReportPermission(AbstractActionInput actionInput, String reportName)
{
try
{
checkReportPermissionThrowing(actionInput, reportName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName());
}
/*******************************************************************************
**
*******************************************************************************/
public static boolean hasWidgetPermission(AbstractActionInput actionInput, String widgetName)
{
try
{
checkWidgetPermissionThrowing(actionInput, widgetName);
return (true);
}
catch(QPermissionDeniedException e)
{
return (false);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static Collection<String> getAllAvailablePermissionNames(QInstance instance)
{
return (getAllAvailablePermissions(instance).stream()
.map(AvailablePermission::getName)
.collect(Collectors.toCollection(LinkedHashSet::new)));
}
/*******************************************************************************
**
*******************************************************************************/
public static Collection<AvailablePermission> getAllAvailablePermissions(QInstance instance)
{
Collection<AvailablePermission> rs = new LinkedHashSet<>();
for(QTableMetaData tableMetaData : instance.getTables().values())
{
if(tableMetaData.getIsHidden())
{
continue;
}
QPermissionRules rules = getEffectivePermissionRules(tableMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, tableMetaData.getName());
for(TablePermissionSubType permissionSubType : TablePermissionSubType.values())
{
addEffectiveAvailablePermission(rules, permissionSubType, rs, baseName, tableMetaData, "Table");
}
}
for(QProcessMetaData processMetaData : instance.getProcesses().values())
{
if(processMetaData.getIsHidden())
{
continue;
}
QPermissionRules rules = getEffectivePermissionRules(processMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, processMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, processMetaData, "Process");
}
for(QAppMetaData appMetaData : instance.getApps().values())
{
QPermissionRules rules = getEffectivePermissionRules(appMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, appMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, appMetaData, "App");
}
for(QReportMetaData reportMetaData : instance.getReports().values())
{
QPermissionRules rules = getEffectivePermissionRules(reportMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, reportMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, reportMetaData, "Report");
}
for(QWidgetMetaDataInterface widgetMetaData : instance.getWidgets().values())
{
QPermissionRules rules = getEffectivePermissionRules(widgetMetaData, instance);
String baseName = getEffectivePermissionBaseName(rules, widgetMetaData.getName());
addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget");
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
private static void addEffectiveAvailablePermission(QPermissionRules rules, PermissionSubType permissionSubType, Collection<AvailablePermission> rs, String baseName, MetaDataWithName metaDataWithName, String objectType)
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
if(effectivePermissionSubType != null)
{
rs.add(new AvailablePermission()
.withName(getPermissionName(baseName, effectivePermissionSubType))
.withObjectName(metaDataWithName.getLabel())
.withPermissionType(effectivePermissionSubType.toString())
.withObjectType(objectType));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static QPermissionRules getEffectivePermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules, QInstance instance)
{
if(metaDataWithPermissionRules.getPermissionRules() == null)
{
LOG.warn("Null permission rules on meta data object [" + metaDataWithPermissionRules.getClass().getSimpleName() + "][" + metaDataWithPermissionRules.getName() + "] - does the instance need enriched? Returning instance default rules.");
return (instance.getDefaultPermissionRules());
}
return (metaDataWithPermissionRules.getPermissionRules());
}
/*******************************************************************************
**
*******************************************************************************/
static boolean hasPermission(QSession session, String permissionBaseName, PermissionSubType permissionSubType)
{
if(permissionSubType == null)
{
return (true);
}
String permissionName = getPermissionName(permissionBaseName, permissionSubType);
return (session.hasPermission(permissionName));
}
/*******************************************************************************
**
*******************************************************************************/
static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, QPermissionRules rules, String permissionBaseName, MetaDataWithPermissionRules metaDataWithPermissionRules, PermissionSubType... permissionSubTypes)
{
for(PermissionSubType permissionSubType : permissionSubTypes)
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
if(rules.getCustomPermissionChecker() != null)
{
try
{
CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, rules.getCustomPermissionChecker());
customPermissionChecker.checkPermissionsThrowing(actionInput, metaDataWithPermissionRules);
return (PermissionCheckResult.ALLOW);
}
catch(QPermissionDeniedException e)
{
return (getPermissionDeniedCheckResult(rules));
}
}
if(hasPermission(QContext.getQSession(), permissionBaseName, effectivePermissionSubType))
{
return (PermissionCheckResult.ALLOW);
}
}
return (getPermissionDeniedCheckResult(rules));
}
/*******************************************************************************
**
*******************************************************************************/
static String getEffectivePermissionBaseName(QPermissionRules rules, String standardName)
{
if(rules != null && StringUtils.hasContent(rules.getPermissionBaseName()))
{
return (rules.getPermissionBaseName());
}
return (standardName);
}
/*******************************************************************************
**
*******************************************************************************/
@SuppressWarnings("checkstyle:indentation")
static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType)
{
if(rules == null || rules.getLevel() == null)
{
return (originalPermissionSubType);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if the original permission sub-type is "hasAccess" - then this is a check for a process/report/widget. //
// in that case - never return the table-level read/write/insert/edit/delete options //
////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType))
{
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
default -> PrivatePermissionSubType.HAS_ACCESS;
};
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, this is a table check - so - based on the rules being used for this table, map the requested //
// permission sub-type to what we expect to be set for the table //
////////////////////////////////////////////////////////////////////////////////////////////////////////
return switch(rules.getLevel())
{
case NOT_PROTECTED -> null;
case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS;
case READ_WRITE_PERMISSIONS ->
{
if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType))
{
yield (originalPermissionSubType);
}
else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.WRITE);
}
else if(TablePermissionSubType.READ.equals(originalPermissionSubType))
{
yield (PrivatePermissionSubType.READ);
}
else
{
throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType);
}
}
case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType;
};
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name) throws QPermissionDeniedException
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
String permissionBaseName = getEffectivePermissionBaseName(rules, name);
if(effectivePermissionSubType == null)
{
return;
}
if(!hasPermission(QContext.getQSession(), permissionBaseName, effectivePermissionSubType))
{
// LOG.debug("Throwing permission denied for: " + getPermissionName(permissionBaseName, effectivePermissionSubType) + " for " + QContext.getQSession().getUser());
throw (new QPermissionDeniedException("Permission denied."));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static String getPermissionName(String permissionBaseName, PermissionSubType permissionSubType)
{
return permissionBaseName + "." + permissionSubType.getPermissionSuffix();
}
/*******************************************************************************
**
*******************************************************************************/
private static PermissionCheckResult getPermissionDeniedCheckResult(QPermissionRules rules)
{
if(rules == null || rules.getDenyBehavior() == null || rules.getDenyBehavior().equals(DenyBehavior.HIDDEN))
{
return (PermissionCheckResult.DENY_HIDE);
}
else
{
return (PermissionCheckResult.DENY_DISABLE);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void warnAboutPermissionSubTypeForTables(PermissionSubType permissionSubType)
{
if(permissionSubType == null)
{
return;
}
if(permissionSubType == PrivatePermissionSubType.HAS_ACCESS)
{
LOG.warn("PermissionSubType.HAS_ACCESS should not be checked for a table");
}
}
}

View File

@ -0,0 +1,56 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
/*******************************************************************************
**
*******************************************************************************/
enum PrivatePermissionSubType implements PermissionSubType
{
HAS_ACCESS("hasAccess"), // for processes, reports, etc - basically, not tables.
READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode
WRITE("write");
private final String permissionSuffix;
/*******************************************************************************
**
*******************************************************************************/
PrivatePermissionSubType(String permissionSuffix)
{
this.permissionSuffix = permissionSuffix;
}
/*******************************************************************************
** Getter for permissionSuffix
*******************************************************************************/
public String getPermissionSuffix()
{
return (this.permissionSuffix);
}
}

View File

@ -0,0 +1,53 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
/*******************************************************************************
**
*******************************************************************************/
public class ReportProcessPermissionChecker implements CustomPermissionChecker
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException
{
if(actionInput instanceof RunProcessInput runProcessInput)
{
String reportName = runProcessInput.getValueString("reportName");
if(reportName != null)
{
PermissionsHelper.checkReportPermissionThrowing(actionInput, reportName);
}
}
}
}

View File

@ -0,0 +1,57 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.permissions;
/*******************************************************************************
**
*******************************************************************************/
public enum TablePermissionSubType implements PermissionSubType
{
READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode
INSERT("insert"), // for table-insert.
EDIT("edit"), // for table-edit.
DELETE("delete"); // for table-delete.
private final String permissionSuffix;
/*******************************************************************************
**
*******************************************************************************/
TablePermissionSubType(String permissionSuffix)
{
this.permissionSuffix = permissionSuffix;
}
/*******************************************************************************
** Getter for permissionSuffix
*******************************************************************************/
public String getPermissionSuffix()
{
return (this.permissionSuffix);
}
}

View File

@ -31,6 +31,7 @@ import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
@ -42,8 +43,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -52,7 +51,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RunBackendStepAction
{
private static final Logger LOG = LogManager.getLogger(RunBackendStepAction.class);
private static final QLogger LOG = QLogger.getLogger(RunBackendStepAction.class);
@ -107,7 +106,7 @@ public class RunBackendStepAction
return;
}
List<QFieldMetaData> fieldsToGet = new ArrayList<>();
List<QFieldMetaData> fieldsToGet = new ArrayList<>();
List<QFieldMetaData> requiredFieldsMissing = new ArrayList<>();
for(QFieldMetaData field : inputMetaData.getFieldList())
{
@ -175,8 +174,7 @@ public class RunBackendStepAction
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{
QueryInput queryInput = new QueryInput(runBackendStepInput.getInstance());
queryInput.setSession(runBackendStepInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
// todo - handle this being async (e.g., http)

View File

@ -31,10 +31,13 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRenderer;
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;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@ -48,20 +51,24 @@ 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;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendComponentMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.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;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -70,7 +77,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RunProcessAction
{
private static final Logger LOG = LogManager.getLogger(RunProcessAction.class);
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
@ -80,6 +87,7 @@ public class RunProcessAction
// indicator that the timestamp field should be updated - e.g., the execute step is finished. //
////////////////////////////////////////////////////////////////////////////////////////////////
public static final String BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD = "basepullReadyToUpdateTimestamp";
public static final String BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD = "basepullDidQueryUsingTimestamp";
@ -142,7 +150,7 @@ public class RunProcessAction
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData)
if(step instanceof QFrontendStepMetaData frontendStep)
{
////////////////////////////////////////////////////////////////
// Handle what to do with frontend steps, per request setting //
@ -152,6 +160,8 @@ public class RunProcessAction
case BREAK ->
{
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, frontendStep);
processFrontendComponents(processState, frontendStep);
processState.setNextStepName(step.getName());
break STEP_LOOP;
}
@ -190,11 +200,14 @@ public class RunProcessAction
}
}
////////////////////////////////////////////////////////////////////////////////////
// if 'basepull' style process, update the stored basepull timestamp //
// but only when we've been signaled to do so - i.e., after an Execute step runs. //
////////////////////////////////////////////////////////////////////////////////////
if(basepullConfiguration != null && BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD))))
///////////////////////////////////////////////////////////////////////////
// if 'basepull' style process, update the stored basepull timestamp //
// but only when we've been signaled to do so - i.e., only if we did our //
// query using the timestamp field, and only after an Execute step runs. //
///////////////////////////////////////////////////////////////////////////
if(basepullConfiguration != null
&& BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_DID_QUERY_USING_TIMESTAMP_FIELD)))
&& BooleanUtils.isTrue(ValueUtils.getValueAsBoolean(runProcessInput.getValue(BASEPULL_READY_TO_UPDATE_TIMESTAMP_FIELD))))
{
storeLastRunTime(runProcessInput, process, basepullConfiguration);
}
@ -226,6 +239,42 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
private void processFrontendComponents(ProcessState processState, QFrontendStepMetaData frontendStep) throws QException
{
for(QFrontendComponentMetaData component : CollectionUtils.nonNullList(frontendStep.getComponents()))
{
if(component instanceof NoCodeWidgetFrontendComponentMetaData noCodeWidgetComponent)
{
NoCodeWidgetRenderer noCodeWidgetRenderer = new NoCodeWidgetRenderer();
Map<String, Object> context = noCodeWidgetRenderer.initContext(null);
context.putAll(processState.getValues());
String html = noCodeWidgetRenderer.renderOutputs(context, noCodeWidgetComponent.getOutputs());
processState.getValues().put(frontendStep.getName() + ".html", html);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void processFrontendStepFieldDefaultValues(ProcessState processState, QFrontendStepMetaData step)
{
for(QFieldMetaData formField : CollectionUtils.mergeLists(step.getFormFields(), step.getInputFields(), step.getViewFields(), step.getOutputFields()))
{
if(formField.getDefaultValue() != null && processState.getValues().get(formField.getName()) == null)
{
processState.getValues().put(formField.getName(), formField.getDefaultValue());
}
}
}
/*******************************************************************************
** When we start running a process (or resuming it), get data in the RunProcessRequest
** either from the state provider (if they're found, for a resume).
@ -291,15 +340,25 @@ public class RunProcessAction
*******************************************************************************/
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getInstance(), processState);
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
runBackendStepInput.setStepName(backendStep.getName());
runBackendStepInput.setTableName(process.getTableName());
runBackendStepInput.setSession(runProcessInput.getSession());
runBackendStepInput.setCallback(runProcessInput.getCallback());
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
runBackendStepInput.setTableName(process.getTableName());
if(!StringUtils.hasContent(runBackendStepInput.getTableName()))
{
////////////////////////////////////////////////////////////////
// help support generic (e.g., not tied-to-a-table) processes //
////////////////////////////////////////////////////////////////
if(runProcessInput.getValue("tableName") != null)
{
runBackendStepInput.setTableName(ValueUtils.getValueAsString(runProcessInput.getValue("tableName")));
}
}
///////////////////////////////////////////////////////////////
// if 'basepull' values are in the inputs, add to step input //
///////////////////////////////////////////////////////////////
@ -427,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.
*******************************************************************************/
@ -435,13 +523,12 @@ 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 //
///////////////////////////////////////
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
queryInput.setSession(runProcessInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()
@ -469,8 +556,7 @@ public class RunProcessAction
////////////
// update //
////////////
UpdateInput updateInput = new UpdateInput(runProcessInput.getInstance());
updateInput.setSession(runProcessInput.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(basepullTableName);
updateInput.setRecords(List.of(basepullRecord));
new UpdateAction().execute(updateInput);
@ -484,8 +570,7 @@ public class RunProcessAction
////////////////////////////////
// insert new basepull record //
////////////////////////////////
InsertInput insertInput = new InsertInput(runProcessInput.getInstance());
insertInput.setSession(runProcessInput.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName(basepullTableName);
insertInput.setRecords(List.of(basepullRecord));
new InsertAction().execute(insertInput);
@ -518,13 +603,12 @@ 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 //
///////////////////////////////////////
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
queryInput.setSession(runProcessInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()

View File

@ -0,0 +1,84 @@
/*
* 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.queues;
import java.util.List;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.GetQueueAttributesResult;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class GetQueueSize
{
private static final QLogger LOG = QLogger.getLogger(GetQueueSize.class);
/*******************************************************************************
**
*******************************************************************************/
public Integer getQueueSize(QQueueProviderMetaData queueProviderMetaData, QQueueMetaData queueMetaData) throws QException
{
try
{
//////////////////////////////////////////////////////////////////
// todo - handle other queue provider types, somewhere, somehow //
//////////////////////////////////////////////////////////////////
SQSQueueProviderMetaData queueProvider = (SQSQueueProviderMetaData) queueProviderMetaData;
BasicAWSCredentials credentials = new BasicAWSCredentials(queueProvider.getAccessKey(), queueProvider.getSecretKey());
final AmazonSQS sqs = AmazonSQSClientBuilder.standard()
.withRegion(queueProvider.getRegion())
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
String queueUrl = queueProvider.getBaseURL();
if(!queueUrl.endsWith("/"))
{
queueUrl += "/";
}
queueUrl += queueMetaData.getQueueName();
GetQueueAttributesResult queueAttributes = sqs.getQueueAttributes(queueUrl, List.of("ApproximateNumberOfMessages"));
String approximateNumberOfMessages = queueAttributes.getAttributes().get("ApproximateNumberOfMessages");
return (Integer.parseInt(approximateNumberOfMessages));
}
catch(Exception e)
{
LOG.warn("Error getting queue size", e, logPair("queueName", queueMetaData == null ? "null" : queueMetaData.getName()));
throw (new QException("Error getting queue size", e));
}
}
}

View File

@ -22,24 +22,27 @@
package com.kingsrook.qqq.backend.core.actions.queues;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.DeleteMessageRequest;
import com.amazonaws.services.sqs.model.DeleteMessageBatchRequest;
import com.amazonaws.services.sqs.model.DeleteMessageBatchRequestEntry;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -47,7 +50,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class SQSQueuePoller implements Runnable
{
private static final Logger LOG = LogManager.getLogger(SQSQueuePoller.class);
private static final QLogger LOG = QLogger.getLogger(SQSQueuePoller.class);
///////////////////////////////////////////////
// todo - move these 2 to a "QBaseRunnable"? //
@ -66,6 +69,12 @@ public class SQSQueuePoller implements Runnable
@Override
public void run()
{
QContext.init(qInstance, sessionSupplier.get());
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName("SQSPoller>" + queueMetaData.getName());
LOG.debug("Running " + this.getClass().getSimpleName() + "[" + queueMetaData.getName() + "]");
try
{
BasicAWSCredentials credentials = new BasicAWSCredentials(queueProviderMetaData.getAccessKey(), queueProviderMetaData.getSecretKey());
@ -83,8 +92,13 @@ public class SQSQueuePoller implements Runnable
while(true)
{
///////////////////////////////
// fetch a batch of messages //
///////////////////////////////
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest();
receiveMessageRequest.setQueueUrl(queueUrl);
receiveMessageRequest.setMaxNumberOfMessages(10);
receiveMessageRequest.setWaitTimeSeconds(20); // help urge SQS to query multiple servers and find more messages
ReceiveMessageResult receiveMessageResult = sqs.receiveMessage(receiveMessageRequest);
if(receiveMessageResult.getMessages().isEmpty())
{
@ -93,31 +107,71 @@ public class SQSQueuePoller implements Runnable
}
LOG.debug(receiveMessageResult.getMessages().size() + " messages received. Processing.");
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// extract data from the messages into list of bodies to pass into process, and list of delete-batch-inputs //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<DeleteMessageBatchRequestEntry> deleteRequestEntries = new ArrayList<>();
ArrayList<String> bodies = new ArrayList<>();
int i = 0;
for(Message message : receiveMessageResult.getMessages())
{
String body = message.getBody();
bodies.add(message.getBody());
deleteRequestEntries.add(new DeleteMessageBatchRequestEntry(String.valueOf(i++), message.getReceiptHandle()));
}
RunProcessInput runProcessInput = new RunProcessInput(qInstance);
runProcessInput.setSession(sessionSupplier.get());
/////////////////////////////////////////////////////////////////////////////////////
// run the process, in a try-catch, so even if it fails, our loop keeps going. //
// the messages in a failed process will get re-delivered, to try-again, up to the //
// number of times configured in AWS //
/////////////////////////////////////////////////////////////////////////////////////
try
{
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(queueMetaData.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.addValue("body", body);
runProcessInput.addValue("bodies", bodies);
QContext.pushAction(runProcessInput);
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
/////////////////////////////////
// todo - what of exceptions?? //
/////////////////////////////////
String receiptHandle = message.getReceiptHandle();
sqs.deleteMessage(new DeleteMessageRequest(queueUrl, receiptHandle));
////////////////////////////////////////////////////////////////////////////////////////////
// if there was an exception returned by the process (e.g., thrown in backend step), then //
// warn and leave the messages for re-processing. //
////////////////////////////////////////////////////////////////////////////////////////////
if(runProcessOutput.getException().isPresent())
{
LOG.warn("Exception returned by process when handling SQS Messages. They will not be deleted from the queue.", runProcessOutput.getException().get());
}
else
{
///////////////////////////////////////////////
// else, if no exception, do a batch delete. //
///////////////////////////////////////////////
sqs.deleteMessageBatch(new DeleteMessageBatchRequest()
.withQueueUrl(queueUrl)
.withEntries(deleteRequestEntries));
}
}
catch(Exception e)
{
LOG.warn("Error receiving SQS Messages.", e);
}
finally
{
QContext.popAction();
}
}
}
catch(Exception e)
{
LOG.warn("Error receiving SQS Message", e);
LOG.warn("Error running SQS Queue Poller", e);
}
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}

View File

@ -0,0 +1,90 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Subclass of RecordPipe, which uses a buffer in the addRecord method, to avoid
** sending single-records at a time through postRecordActions and to consumers.
*******************************************************************************/
public class BufferedRecordPipe extends RecordPipe
{
private List<QRecord> buffer = new ArrayList<>();
private Integer bufferSize = 100;
/*******************************************************************************
** Constructor - uses default buffer size
**
*******************************************************************************/
public BufferedRecordPipe()
{
}
/*******************************************************************************
** Constructor - customize buffer size.
**
*******************************************************************************/
public BufferedRecordPipe(Integer bufferSize)
{
this.bufferSize = bufferSize;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecord(QRecord record) throws QException
{
buffer.add(record);
if(buffer.size() >= bufferSize)
{
addRecords(buffer);
buffer.clear();
}
}
/*******************************************************************************
**
*******************************************************************************/
public void finalFlush() throws QException
{
if(!buffer.isEmpty())
{
addRecords(buffer);
buffer.clear();
}
}
}

View File

@ -27,13 +27,12 @@ import java.nio.charset.StandardCharsets;
import java.util.List;
import com.kingsrook.qqq.backend.core.adapters.QRecordToCsvAdapter;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -41,7 +40,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class CsvExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(CsvExportStreamer.class);
private static final QLogger LOG = QLogger.getLogger(CsvExportStreamer.class);
private final QRecordToCsvAdapter qRecordToCsvAdapter;

View File

@ -37,6 +37,7 @@ import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -45,8 +46,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dhatim.fastexcel.StyleSetter;
import org.dhatim.fastexcel.Workbook;
import org.dhatim.fastexcel.Worksheet;
@ -57,7 +56,7 @@ import org.dhatim.fastexcel.Worksheet;
*******************************************************************************/
public class ExcelExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ExcelExportStreamer.class);
private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
@ -247,14 +246,6 @@ public class ExcelExportStreamer implements ExportStreamerInterface
for(QFieldMetaData field : fields)
{
Serializable value = qRecord.getValue(field.getName());
if(field.getPossibleValueSourceName() != null)
{
String displayValue = qRecord.getDisplayValue(field.getName());
if(displayValue != null)
{
value = displayValue;
}
}
if(value != null)
{

View File

@ -23,34 +23,42 @@ package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.interfaces.CountInterface;
import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -66,7 +74,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class ExportAction
{
private static final Logger LOG = LogManager.getLogger(ExportAction.class);
private static final QLogger LOG = QLogger.getLogger(ExportAction.class);
private boolean preExecuteRan = false;
private Integer countFromPreExecute = null;
@ -95,15 +103,25 @@ public class ExportAction
///////////////////////////////////
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
QTableMetaData table = exportInput.getTable();
List<String> badFieldNames = new ArrayList<>();
QTableMetaData table = exportInput.getTable();
Map<String, QTableMetaData> joinTableMap = getJoinTableMap(table);
List<String> badFieldNames = new ArrayList<>();
for(String fieldName : exportInput.getFieldNames())
{
try
{
table.getField(fieldName);
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.", 2);
joinTableMap.get(parts[0]).getField(parts[1]);
}
else
{
table.getField(fieldName);
}
}
catch(IllegalArgumentException iae)
catch(Exception e)
{
badFieldNames.add(fieldName);
}
@ -128,6 +146,21 @@ public class ExportAction
/*******************************************************************************
**
*******************************************************************************/
private static Map<String, QTableMetaData> getJoinTableMap(QTableMetaData table)
{
Map<String, QTableMetaData> joinTableMap = new HashMap<>();
for(ExposedJoin exposedJoin : CollectionUtils.nonNullList(table.getExposedJoins()))
{
joinTableMap.put(exposedJoin.getJoinTable(), QContext.getQInstance().getTable(exposedJoin.getJoinTable()));
}
return joinTableMap;
}
/*******************************************************************************
** Run the report.
*******************************************************************************/
@ -147,17 +180,63 @@ public class ExportAction
//////////////////////////
// set up a query input //
//////////////////////////
QueryInterface queryInterface = backendModule.getQueryInterface();
QueryInput queryInput = new QueryInput(exportInput.getInstance());
queryInput.setSession(exportInput.getSession());
QueryAction queryAction = new QueryAction();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(exportInput.getTableName());
queryInput.setFilter(exportInput.getQueryFilter());
queryInput.setLimit(exportInput.getLimit());
List<QueryJoin> queryJoins = new ArrayList<>();
Set<String> addedJoinNames = new HashSet<>();
if(CollectionUtils.nullSafeHasContents(exportInput.getFieldNames()))
{
for(String fieldName : exportInput.getFieldNames())
{
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.", 2);
String joinTableName = parts[0];
if(!addedJoinNames.contains(joinTableName))
{
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);
}
}
}
}
queryInput.setQueryJoins(queryJoins);
if(queryInput.getFilter() == null)
{
queryInput.setFilter(new QQueryFilter());
}
queryInput.getFilter().setLimit(exportInput.getLimit());
queryInput.setShouldTranslatePossibleValues(true);
/////////////////////////////////////////////////////////////////
// tell this query that it needs to put its output into a pipe //
/////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new RecordPipe();
RecordPipe recordPipe = new BufferedRecordPipe(500);
queryInput.setRecordPipe(recordPipe);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -165,13 +244,14 @@ public class ExportAction
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ReportFormat reportFormat = exportInput.getReportFormat();
ExportStreamerInterface reportStreamer = reportFormat.newReportStreamer();
reportStreamer.start(exportInput, getFields(exportInput), "Sheet 1");
List<QFieldMetaData> fields = getFields(exportInput);
reportStreamer.start(exportInput, fields, "Sheet 1");
//////////////////////////////////////////
// run the query action as an async job //
//////////////////////////////////////////
AsyncJobManager asyncJobManager = new AsyncJobManager();
String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryInterface.execute(queryInput)));
String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> (queryAction.execute(queryInput)));
LOG.info("Started query job [" + queryJobUUID + "] for report");
AsyncJobState queryJobState = AsyncJobState.RUNNING;
@ -209,7 +289,7 @@ public class ExportAction
nextSleepMillis = INIT_SLEEP_MS;
List<QRecord> records = recordPipe.consumeAvailableRecords();
reportStreamer.addRecords(records);
processRecords(reportStreamer, fields, records);
recordCount += records.size();
LOG.info(countFromPreExecute != null
@ -238,7 +318,7 @@ public class ExportAction
// send the final records to the report streamer //
///////////////////////////////////////////////////
List<QRecord> records = recordPipe.consumeAvailableRecords();
reportStreamer.addRecords(records);
processRecords(reportStreamer, fields, records);
recordCount += records.size();
long reportEndTime = System.currentTimeMillis();
@ -269,20 +349,86 @@ public class ExportAction
/*******************************************************************************
**
*******************************************************************************/
private static void processRecords(ExportStreamerInterface reportStreamer, List<QFieldMetaData> fields, List<QRecord> records) throws QReportingException
{
for(QFieldMetaData field : fields)
{
if(field.getName().endsWith(":possibleValueLabel"))
{
String effectiveFieldName = field.getName().replace(":possibleValueLabel", "");
for(QRecord record : records)
{
String displayValue = record.getDisplayValue(effectiveFieldName);
record.setValue(field.getName(), displayValue);
}
}
}
reportStreamer.addRecords(records);
}
/*******************************************************************************
**
*******************************************************************************/
private List<QFieldMetaData> getFields(ExportInput exportInput)
{
QTableMetaData table = exportInput.getTable();
QTableMetaData table = exportInput.getTable();
Map<String, QTableMetaData> joinTableMap = getJoinTableMap(table);
List<QFieldMetaData> fieldList;
if(exportInput.getFieldNames() != null)
{
return (exportInput.getFieldNames().stream().map(table::getField).toList());
fieldList = new ArrayList<>();
for(String fieldName : exportInput.getFieldNames())
{
if(fieldName.contains("."))
{
String[] parts = fieldName.split("\\.", 2);
QTableMetaData joinTable = joinTableMap.get(parts[0]);
QFieldMetaData field = joinTable.getField(parts[1]).clone();
field.setName(fieldName);
field.setLabel(joinTable.getLabel() + ": " + field.getLabel());
fieldList.add(field);
}
else
{
fieldList.add(table.getField(fieldName));
}
}
}
else
{
return (new ArrayList<>(table.getFields().values()));
fieldList = new ArrayList<>(table.getFields().values());
}
List<QFieldMetaData> returnList = new ArrayList<>();
for(QFieldMetaData field : fieldList)
{
/////////////////////////////////////////////////////////////////////////////////////////
// skip heavy fields. they aren't fetched, and we generally think we don't want them. //
/////////////////////////////////////////////////////////////////////////////////////////
if(field.getIsHeavy())
{
continue;
}
returnList.add(field);
//////////////////////////////////////////
// add fields for possible value labels //
//////////////////////////////////////////
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
{
returnList.add(new QFieldMetaData(field.getName() + ":possibleValueLabel", QFieldType.STRING).withLabel(field.getLabel() + " Name"));
}
}
return (returnList);
}
@ -308,8 +454,7 @@ public class ExportAction
if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows())
{
CountInterface countInterface = backendModule.getCountInterface();
CountInput countInput = new CountInput(exportInput.getInstance());
countInput.setSession(exportInput.getSession());
CountInput countInput = new CountInput();
countInput.setTableName(exportInput.getTableName());
countInput.setFilter(exportInput.getQueryFilter());
CountOutput countOutput = countInterface.execute(countInput);

View File

@ -185,7 +185,7 @@ public class FormulaInterpreter
case "DIVIDE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 2, variableInterpreter);
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
if(numbers.get(1) == null || numbers.get(1).compareTo(BigDecimal.ZERO) == 0)
{
return null;
}
@ -194,7 +194,7 @@ public class FormulaInterpreter
case "DIVIDE_SCALE":
{
List<BigDecimal> numbers = getNumberArgumentList(args, 3, variableInterpreter);
if(numbers.get(1) == null || numbers.get(1).equals(BigDecimal.ZERO))
if(numbers.get(1) == null || numbers.get(1).compareTo(BigDecimal.ZERO) == 0)
{
return null;
}

View File

@ -27,13 +27,17 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.DataSourceQueryInputCustomizer;
import com.kingsrook.qqq.backend.core.actions.reporting.customizers.ReportViewCustomizer;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
@ -41,12 +45,13 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
@ -60,10 +65,11 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.BackendStepPostRunInput;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.BackendStepPostRunOutput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.aggregates.AggregatesInterface;
import com.kingsrook.qqq.backend.core.utils.aggregates.BigDecimalAggregates;
import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
@ -83,6 +89,8 @@ import com.kingsrook.qqq.backend.core.utils.aggregates.IntegerAggregates;
*******************************************************************************/
public class GenerateReportAction
{
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// summaryAggregates and varianceAggregates are multi-level maps, ala: //
// viewName > SummaryKey > fieldName > Aggregates //
@ -112,6 +120,10 @@ public class GenerateReportAction
{
report = reportInput.getInstance().getReport(reportInput.getReportName());
reportFormat = reportInput.getReportFormat();
if(reportFormat == null)
{
throw new QException("Report format was not specified.");
}
reportStreamer = reportFormat.newReportStreamer();
////////////////////////////////////////////////////////////////////////////////////////////////
@ -178,6 +190,17 @@ public class GenerateReportAction
}
outputSummaries(reportInput);
reportStreamer.finish();
try
{
reportInput.getReportOutputStream().close();
}
catch(Exception e)
{
throw (new QReportingException("Error completing report", e));
}
}
@ -185,46 +208,52 @@ public class GenerateReportAction
/*******************************************************************************
**
*******************************************************************************/
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QReportingException
private void startTableView(ReportInput reportInput, QReportDataSource dataSource, QReportView reportView) throws QException
{
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession());
ExportInput exportInput = new ExportInput();
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
exportInput.setIncludeHeaderRow(reportView.getIncludeHeaderRow());
exportInput.setReportOutputStream(reportInput.getReportOutputStream());
List<QFieldMetaData> fields;
if(CollectionUtils.nullSafeHasContents(reportView.getColumns()))
JoinsContext joinsContext = null;
if(StringUtils.hasContent(dataSource.getSourceTable()))
{
fields = new ArrayList<>();
for(QReportField column : reportView.getColumns())
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
}
List<QFieldMetaData> fields = new ArrayList<>();
for(QReportField column : reportView.getColumns())
{
if(column.getIsVirtual())
{
if(column.getIsVirtual())
fields.add(column.toField());
}
else
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
JoinsContext.FieldAndTableNameOrAlias fieldAndTableNameOrAlias = joinsContext == null ? null : joinsContext.getFieldAndTableNameOrAlias(effectiveFieldName);
if(fieldAndTableNameOrAlias == null || fieldAndTableNameOrAlias.field() == null)
{
fields.add(column.toField());
throw new QReportingException("Could not find field named [" + effectiveFieldName + "] in dataSource [" + dataSource.getName() + "]");
}
else
QFieldMetaData field = fieldAndTableNameOrAlias.field().clone();
field.setName(column.getName());
if(StringUtils.hasContent(column.getLabel()))
{
QFieldMetaData field = table.getField(column.getName()).clone();
if(StringUtils.hasContent(column.getLabel()))
{
field.setLabel(column.getLabel());
}
fields.add(field);
field.setLabel(column.getLabel());
}
fields.add(field);
}
}
else
{
fields = new ArrayList<>(table.getFields().values());
}
reportStreamer.setDisplayFormats(getDisplayFormatMap(fields));
reportStreamer.start(exportInput, fields, reportView.getLabel());
}
@ -246,8 +275,7 @@ public class GenerateReportAction
{
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStepInput = new RunBackendStepInput(reportInput.getInstance());
transformStepInput.setSession(reportInput.getSession());
transformStepInput = new RunBackendStepInput();
transformStepInput.setValues(reportInput.getInputValues());
transformStepOutput = new RunBackendStepOutput();
@ -265,20 +293,29 @@ public class GenerateReportAction
/////////////////////////////////////////////////////////////////
// run a record pipe loop, over the query for this data source //
/////////////////////////////////////////////////////////////////
RecordPipe recordPipe = new RecordPipe();
RecordPipe recordPipe = new BufferedRecordPipe(1000);
new AsyncRecordPipeLoop().run("Report[" + reportInput.getReportName() + "]", null, recordPipe, (callback) ->
{
if(dataSource.getSourceTable() != null)
{
QQueryFilter queryFilter = dataSource.getQueryFilter().clone();
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
QueryInput queryInput = new QueryInput(reportInput.getInstance());
queryInput.setSession(reportInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setRecordPipe(recordPipe);
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setShouldTranslatePossibleValues(true); // todo - any limits or conditions on this?
queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
if(dataSource.getQueryInputCustomizer() != null)
{
DataSourceQueryInputCustomizer queryInputCustomizer = QCodeLoader.getAdHoc(DataSourceQueryInputCustomizer.class, dataSource.getQueryInputCustomizer());
queryInput = queryInputCustomizer.run(reportInput, queryInput);
}
return (new QueryAction().execute(queryInput));
}
else if(dataSource.getStaticDataSupplier() != null)
@ -320,12 +357,51 @@ public class GenerateReportAction
////////////////////////////////////////////////
if(transformStep != null)
{
transformStep.postRun(transformStepInput, transformStepOutput);
transformStep.postRun(new BackendStepPostRunInput(transformStepInput), new BackendStepPostRunOutput(transformStepOutput));
}
}
/*******************************************************************************
**
*******************************************************************************/
private Set<String> setupFieldsToTranslatePossibleValues(ReportInput reportInput, QReportDataSource dataSource, JoinsContext joinsContext)
{
Set<String> fieldsToTranslatePossibleValues = new HashSet<>();
for(QReportView view : report.getViews())
{
for(QReportField column : CollectionUtils.nonNullList(view.getColumns()))
{
////////////////////////////////////////////////////////////////////////////////////////
// if this is a column marked as ShowPossibleValueLabel, then we need to translate it //
////////////////////////////////////////////////////////////////////////////////////////
if(column.getShowPossibleValueLabel())
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
fieldsToTranslatePossibleValues.add(effectiveFieldName);
}
}
for(String summaryField : CollectionUtils.nonNullList(view.getPivotFields()))
{
///////////////////////////////////////////////////////////////////////////////
// all pivotFields that are possible value sources are implicitly translated //
///////////////////////////////////////////////////////////////////////////////
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
fieldsToTranslatePossibleValues.add(summaryField);
}
}
}
return (fieldsToTranslatePossibleValues);
}
/*******************************************************************************
**
*******************************************************************************/
@ -336,23 +412,7 @@ public class GenerateReportAction
return;
}
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
for(QFilterCriteria criterion : queryFilter.getCriteria())
{
if(criterion.getValues() != null)
{
List<Serializable> newValues = new ArrayList<>();
for(Serializable value : criterion.getValues())
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpret(valueAsString);
newValues.add(interpretedValue);
}
criterion.setValues(newValues);
}
}
queryFilter.interpretValues(reportInput.getInputValues());
}
@ -369,6 +429,22 @@ public class GenerateReportAction
////////////////////////////////////////////////////////////////////////////
if(tableView != null)
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if any fields are 'showPossibleValueLabel', then move display values for them into the record's values map //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QReportField column : tableView.getColumns())
{
if(column.getShowPossibleValueLabel())
{
String effectiveFieldName = Objects.requireNonNullElse(column.getSourceFieldName(), column.getName());
for(QRecord record : records)
{
String displayValue = record.getDisplayValue(effectiveFieldName);
record.setValue(column.getName(), displayValue);
}
}
}
reportStreamer.addRecords(records);
}
@ -430,6 +506,9 @@ public class GenerateReportAction
Serializable summaryValue = record.getValue(summaryField);
if(table.getField(summaryField).getPossibleValueSourceName() != null)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// so, this is kinda a thing - where we implicitly use possible-value labels (e.g., display values) for pivot fields... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
summaryValue = record.getDisplayValue(summaryField);
}
key.add(summaryField, summaryValue);
@ -498,8 +577,7 @@ public class GenerateReportAction
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession());
ExportInput exportInput = new ExportInput();
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(summaryOutput.titleRow);
@ -516,8 +594,6 @@ public class GenerateReportAction
reportStreamer.addTotalsRow(summaryOutput.totalRow);
}
}
reportStreamer.finish();
}

View File

@ -0,0 +1,177 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
** JSON export format implementation
*******************************************************************************/
public class JsonExportStreamer implements ExportStreamerInterface
{
private static final QLogger LOG = QLogger.getLogger(JsonExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private boolean needComma = false;
private boolean prettyPrint = true;
/*******************************************************************************
**
*******************************************************************************/
public JsonExportStreamer()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void start(ExportInput exportInput, List<QFieldMetaData> fields, String label) throws QReportingException
{
this.exportInput = exportInput;
this.fields = fields;
table = exportInput.getTable();
outputStream = this.exportInput.getReportOutputStream();
try
{
outputStream.write('[');
}
catch(IOException e)
{
throw (new QReportingException("Error starting report output", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addRecords(List<QRecord> qRecords) throws QReportingException
{
LOG.info("Consuming [" + qRecords.size() + "] records from the pipe");
for(QRecord qRecord : qRecords)
{
writeRecord(qRecord);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void writeRecord(QRecord qRecord) throws QReportingException
{
try
{
if(needComma)
{
outputStream.write(',');
}
Map<String, Serializable> mapForJson = new LinkedHashMap<>();
for(QFieldMetaData field : fields)
{
String labelForJson = StringUtils.lcFirst(field.getLabel().replace(" ", ""));
mapForJson.put(labelForJson, qRecord.getValue(field.getName()));
}
String json = prettyPrint ? JsonUtils.toPrettyJson(mapForJson) : JsonUtils.toJson(mapForJson);
if(prettyPrint)
{
outputStream.write('\n');
}
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
outputStream.flush(); // todo - less often?
needComma = true;
}
catch(Exception e)
{
throw (new QReportingException("Error writing JSON report", e));
}
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void addTotalsRow(QRecord record) throws QReportingException
{
writeRecord(record);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void finish() throws QReportingException
{
try
{
if(prettyPrint)
{
outputStream.write('\n');
}
outputStream.write(']');
}
catch(IOException e)
{
throw (new QReportingException("Error ending report output", e));
}
}
}

View File

@ -27,11 +27,10 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -41,7 +40,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class ListOfMapsExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ListOfMapsExportStreamer.class);
private static final QLogger LOG = QLogger.getLogger(ListOfMapsExportStreamer.class);
private ExportInput exportInput;
private List<QFieldMetaData> fields;

View File

@ -26,11 +26,11 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeConsumer;
/*******************************************************************************
@ -39,7 +39,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RecordPipe
{
private static final Logger LOG = LogManager.getLogger(RecordPipe.class);
private static final QLogger LOG = QLogger.getLogger(RecordPipe.class);
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes
@ -48,7 +48,7 @@ public class RecordPipe
private boolean isTerminated = false;
private Consumer<List<QRecord>> postRecordActions = null;
private UnsafeConsumer<List<QRecord>, QException> postRecordActions = null;
/////////////////////////////////////
// See usage below for explanation //
@ -57,6 +57,26 @@ public class RecordPipe
/*******************************************************************************
** Default constructor.
*******************************************************************************/
public RecordPipe()
{
}
/*******************************************************************************
** Construct a record pipe, with an alternative capacity for the internal queue.
*******************************************************************************/
public RecordPipe(Integer overrideCapacity)
{
queue = new ArrayBlockingQueue<>(overrideCapacity);
}
/*******************************************************************************
** Turn off the pipe. Stop accepting new records (just ignore them in the add
** method). Clear the existing queue. Don't return any more records. Note that
@ -74,7 +94,7 @@ public class RecordPipe
/*******************************************************************************
** Add a record to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecord(QRecord record)
public void addRecord(QRecord record) throws QException
{
if(isTerminated)
{
@ -90,7 +110,7 @@ public class RecordPipe
// (which we'll create as a field in this class, to avoid always re-constructing) //
////////////////////////////////////////////////////////////////////////////////////
singleRecordListForPostRecordActions.add(record);
postRecordActions.accept(singleRecordListForPostRecordActions);
postRecordActions.run(singleRecordListForPostRecordActions);
record = singleRecordListForPostRecordActions.remove(0);
}
@ -109,6 +129,7 @@ public class RecordPipe
if(!offerResult && !isTerminated)
{
LOG.debug("Pipe is full. Waiting.");
long sleepLoopStartTime = System.currentTimeMillis();
long now = System.currentTimeMillis();
while(!offerResult && !isTerminated)
@ -123,6 +144,7 @@ public class RecordPipe
offerResult = queue.offer(record);
now = System.currentTimeMillis();
}
LOG.debug("Pipe has opened up. Resuming.");
}
}
@ -131,11 +153,11 @@ public class RecordPipe
/*******************************************************************************
** Add a list of records to the pipe. Will block if the pipe is full. Will noop if pipe is terminated.
*******************************************************************************/
public void addRecords(List<QRecord> records)
public void addRecords(List<QRecord> records) throws QException
{
if(postRecordActions != null)
{
postRecordActions.accept(records);
postRecordActions.run(records);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
@ -186,7 +208,7 @@ public class RecordPipe
/*******************************************************************************
**
*******************************************************************************/
public void setPostRecordActions(Consumer<List<QRecord>> postRecordActions)
public void setPostRecordActions(UnsafeConsumer<List<QRecord>, QException> postRecordActions)
{
this.postRecordActions = postRecordActions;
}

View File

@ -0,0 +1,79 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
** Subclass of BufferedRecordPipe, which ultimately sends records down to an
** original RecordPipe.
**
** Meant to be used where: someone passed in a RecordPipe (so they have a reference
** to it, and they are waiting to read from it), but the producer knows that
** it will be better to buffer the records, so they want to use a buffered pipe
** (but they still need the records to end up in the original pipe - thus -
** it gets wrapped by an object of this class).
*******************************************************************************/
public class RecordPipeBufferedWrapper extends BufferedRecordPipe
{
private RecordPipe wrappedPipe;
/*******************************************************************************
** Constructor - uses default buffer size
**
*******************************************************************************/
public RecordPipeBufferedWrapper(RecordPipe wrappedPipe)
{
this.wrappedPipe = wrappedPipe;
}
/*******************************************************************************
** Constructor - customize buffer size.
**
*******************************************************************************/
public RecordPipeBufferedWrapper(Integer bufferSize, RecordPipe wrappedPipe)
{
super(bufferSize);
this.wrappedPipe = wrappedPipe;
}
/*******************************************************************************
** when it's time to actually add records into the pipe, actually add them
** into the wrapped pipe!
*******************************************************************************/
@Override
public void addRecords(List<QRecord> records) throws QException
{
wrappedPipe.addRecords(records);
}
}

View File

@ -0,0 +1,45 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.reporting.customizers;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
/*******************************************************************************
** Interface for customizer on a QReportDataSource's query.
**
** Useful, for example, to look at what input field values were given, and change
** the query filter (e.g., conditional criteria), or issue an error based on the
** combination of input fields given.
*******************************************************************************/
public interface DataSourceQueryInputCustomizer
{
/*******************************************************************************
**
*******************************************************************************/
QueryInput run(ReportInput reportInput, QueryInput queryInput) throws QException;
}

View File

@ -23,30 +23,19 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
/*******************************************************************************
** Unit test for TestScriptAction
**
*******************************************************************************/
class TestScriptActionTest
public interface AssociatedScriptContextPrimerInterface
{
/*******************************************************************************
**
*******************************************************************************/
@Test
@Disabled("Not yet done.")
void test() throws QException
{
QInstance instance = TestUtils.defineInstance();
TestScriptInput input = new TestScriptInput(instance);
TestScriptOutput output = new TestScriptOutput();
new TestScriptAction().run(input, output);
}
void primeContext(ExecuteCodeInput executeCodeInput, ScriptRevision scriptRevision) throws QException;
}
}

View File

@ -23,15 +23,36 @@ 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;
/*******************************************************************************
@ -49,6 +70,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
*******************************************************************************/
public class ExecuteCodeAction
{
private static final QLogger LOG = QLogger.getLogger(ExecuteCodeAction.class);
/*******************************************************************************
**
@ -68,10 +92,10 @@ public class ExecuteCodeAction
try
{
String languageExecutor = switch(codeReference.getCodeType())
{
case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
};
{
case JAVA -> "com.kingsrook.qqq.backend.core.actions.scripts.QJavaExecutor";
case JAVA_SCRIPT -> "com.kingsrook.qqq.languages.javascript.QJavaScriptExecutor";
};
@SuppressWarnings("unchecked")
Class<? extends QCodeExecutor> executorClass = (Class<? extends QCodeExecutor>) Class.forName(languageExecutor);
@ -90,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);
@ -108,6 +148,156 @@ public class ExecuteCodeAction
/*******************************************************************************
**
*******************************************************************************/
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)));
executeCodeInput.setContext(new HashMap<>());
Map<String, Serializable> context = executeCodeInput.getContext();
if(input.getOutputObject() != null)
{
context.put("output", input.getOutputObject());
}
if(input.getScriptUtils() != null)
{
context.put("scriptUtils", input.getScriptUtils());
}
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());
ExecuteCodeAction.setExecutionLoggerInExecuteCodeInput(input, scriptRevision, executeCodeInput);
return (executeCodeInput);
}
/*******************************************************************************
** 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, ScriptRevision scriptRevision)
{
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;
}
try
{
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Object apiScriptUtilsObject = apiScriptUtilsClass.getConstructor(String.class, String.class).newInstance(apiName, apiVersion);
context.put("api", (Serializable) apiScriptUtilsObject);
}
catch(ClassNotFoundException e)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?");
}
catch(Exception e)
{
LOG.warn("Error adding api utility to script context", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static void setExecutionLoggerInExecuteCodeInput(AbstractRunScriptInput<?> input, ScriptRevision scriptRevision, ExecuteCodeInput executeCodeInput)
{
/////////////////////////////////////////////////////////////////////////////////////////////////
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
/////////////////////////////////////////////////////////////////////////////////////////////////
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
executeCodeInput.setExecutionLogger(executionLogger);
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
////////////////////////////////////////////////////////////////////////////////////////////////////
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
}
}
/*******************************************************************************
**
*******************************************************************************/

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,245 @@
/*
* 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.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
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.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Object made available to scripts for access to qqq api (e.g., query, insert,
** etc, plus object constructors).
**
** Before scripts knew about the API, this class made sense and was used.
** But, when scripts gained knowledge of the API, then it felt like this class could
** be deleted... but, what about, a QQQ deployment without the API module...
** In that case, we might still want this class... think about it.
**
** And/Or - it turns out - sometimes using QQQ directly is "better" (?) than using
** an api - so - this object may be available for other use cases (e.g., getting
** a record's backendDetails (e.g., for full json from a source backend api)).
*******************************************************************************/
public class QqqScriptUtils implements Serializable
{
/*******************************************************************************
**
*******************************************************************************/
public QueryInput newQueryInput()
{
return (new QueryInput());
}
/*******************************************************************************
**
*******************************************************************************/
public QQueryFilter newQueryFilter()
{
return (new QQueryFilter());
}
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteria newFilterCriteria()
{
return (new QFilterCriteria());
}
/*******************************************************************************
**
*******************************************************************************/
public QFilterOrderBy newFilterOrderBy()
{
return (new QFilterOrderBy());
}
/*******************************************************************************
**
*******************************************************************************/
public QRecord newRecord()
{
return (new QRecord());
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> query(String tableName, QQueryFilter filter) throws QException
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(filter);
PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);
return (new QueryAction().execute(queryInput).getRecords());
}
/*******************************************************************************
**
*******************************************************************************/
public List<QRecord> query(QueryInput queryInput) throws QException
{
PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);
return (new QueryAction().execute(queryInput).getRecords());
}
/*******************************************************************************
**
*******************************************************************************/
public void insert(String tableName, QRecord record) throws QException
{
insert(tableName, List.of(record));
}
/*******************************************************************************
**
*******************************************************************************/
public void insert(String tableName, List<QRecord> recordList) throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(tableName);
insertInput.setRecords(recordList);
PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT);
new InsertAction().execute(insertInput);
}
/*******************************************************************************
**
*******************************************************************************/
public void update(String tableName, QRecord record) throws QException
{
update(tableName, List.of(record));
}
/*******************************************************************************
**
*******************************************************************************/
public void update(String tableName, List<QRecord> recordList) throws QException
{
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(tableName);
updateInput.setRecords(recordList);
PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT);
new UpdateAction().execute(updateInput);
}
/*******************************************************************************
**
*******************************************************************************/
public void delete(String tableName, Serializable primaryKey) throws QException
{
delete(tableName, List.of(primaryKey));
}
/*******************************************************************************
**
*******************************************************************************/
public void delete(String tableName, QRecord record) throws QException
{
delete(tableName, List.of(record));
}
/*******************************************************************************
**
*******************************************************************************/
public void delete(String tableName, List<?> recordOrPrimaryKeyList) throws QException
{
QTableMetaData table = QContext.getQInstance().getTable(tableName);
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(tableName);
List<Serializable> primaryKeyList = new ArrayList<>();
for(Object o : recordOrPrimaryKeyList)
{
if(o instanceof QRecord qRecord)
{
primaryKeyList.add(qRecord.getValue(table.getPrimaryKeyField()));
}
else
{
primaryKeyList.add((Serializable) o);
}
}
deleteInput.setPrimaryKeys(primaryKeyList);
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
new DeleteAction().execute(deleteInput);
}
/*******************************************************************************
**
*******************************************************************************/
public void delete(String tableName, QQueryFilter filter) throws QException
{
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(tableName);
deleteInput.setQueryFilter(filter);
PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE);
new DeleteAction().execute(deleteInput);
}
}

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

@ -0,0 +1,220 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.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.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
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.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.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;
/*******************************************************************************
**
*******************************************************************************/
public class RunAdHocRecordScriptAction
{
private static final QLogger LOG = QLogger.getLogger(RunAdHocRecordScriptAction.class);
private Map<Integer, ScriptRevision> scriptRevisionCacheByScriptRevisionId = new HashMap<>();
private Map<Integer, ScriptRevision> scriptRevisionCacheByScriptId = new HashMap<>();
/*******************************************************************************
**
*******************************************************************************/
public void run(RunAdHocRecordScriptInput input, RunAdHocRecordScriptOutput output) throws QException
{
try
{
ActionHelper.validateSession(input);
/////////////////////////
// figure out the code //
/////////////////////////
ScriptRevision scriptRevision = getScriptRevision(input);
if(scriptRevision == null)
{
throw (new QException("Script revision was not found."));
}
////////////////////////////
// figure out the records //
////////////////////////////
QTableMetaData table = QContext.getQInstance().getTable(input.getTableName());
if(CollectionUtils.nullSafeIsEmpty(input.getRecordList()))
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(input.getTableName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getRecordPrimaryKeyList())));
queryInput.setIncludeAssociations(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
input.setRecordList(queryOutput.getRecords());
}
if(CollectionUtils.nullSafeIsEmpty(input.getRecordList()))
{
////////////////////////////////////////
// just return if nothing found? idk //
////////////////////////////////////////
LOG.info("No records supplied as input (or found via primary keys); exiting with noop");
return;
}
/////////////
// run it! //
/////////////
ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision);
executeCodeInput.getInput().put("records", getRecordsForScript(input, scriptRevision));
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutput(executeCodeOutput.getOutput());
output.setLogger(executeCodeInput.getExecutionLogger());
}
catch(Exception e)
{
output.setException(Optional.of(e));
}
}
/*******************************************************************************
**
*******************************************************************************/
private static ArrayList<? extends Serializable> getRecordsForScript(RunAdHocRecordScriptInput input, ScriptRevision scriptRevision)
{
try
{
Class<?> apiScriptUtilsClass = Class.forName("com.kingsrook.qqq.api.utils.ApiScriptUtils");
Method qRecordListToApiRecordList = apiScriptUtilsClass.getMethod("qRecordListToApiRecordList", List.class, String.class, String.class, String.class);
Object apiRecordList = qRecordListToApiRecordList.invoke(null, input.getRecordList(), input.getTableName(), scriptRevision.getApiName(), scriptRevision.getApiVersion());
// noinspection unchecked
return (ArrayList<? extends Serializable>) apiRecordList;
}
catch(ClassNotFoundException e)
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is the only exception we're kinda expecting here - so catch for it specifically, and just log.trace - others, warn //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
LOG.trace("Couldn't load ApiScriptUtils class - qqq-middleware-api not on the classpath?");
}
catch(Exception e)
{
LOG.warn("Error converting QRecord list to api record list", e);
}
return (new ArrayList<>(input.getRecordList()));
}
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getScriptRevision(RunAdHocRecordScriptInput input) throws QException
{
AdHocScriptCodeReference codeReference = input.getCodeReference();
if(codeReference.getScriptRevisionRecord() != null)
{
return (new ScriptRevision(codeReference.getScriptRevisionRecord()));
}
if(codeReference.getScriptRevisionId() != null)
{
if(!scriptRevisionCacheByScriptRevisionId.containsKey(codeReference.getScriptRevisionId()))
{
GetInput getInput = new GetInput();
getInput.setTableName(ScriptRevision.TABLE_NAME);
getInput.setPrimaryKey(codeReference.getScriptRevisionId());
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() != null)
{
scriptRevisionCacheByScriptRevisionId.put(codeReference.getScriptRevisionId(), new ScriptRevision(getOutput.getRecord()));
}
else
{
scriptRevisionCacheByScriptRevisionId.put(codeReference.getScriptRevisionId(), null);
}
}
return (scriptRevisionCacheByScriptRevisionId.get(codeReference.getScriptRevisionId()));
}
if(codeReference.getScriptId() != null)
{
if(!scriptRevisionCacheByScriptId.containsKey(codeReference.getScriptId()))
{
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(ScriptsMetaDataProvider.CURRENT_SCRIPT_REVISION_JOIN_NAME)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
{
scriptRevisionCacheByScriptId.put(codeReference.getScriptId(), new ScriptRevision(queryOutput.getRecords().get(0)));
}
else
{
scriptRevisionCacheByScriptId.put(codeReference.getScriptId(), null);
}
}
return (scriptRevisionCacheByScriptId.get(codeReference.getScriptId()));
}
throw (new QException("Code reference did not contain a scriptRevision, scriptRevisionId, or scriptId"));
}
}

View File

@ -24,8 +24,8 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
@ -35,8 +35,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptI
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
@ -46,6 +45,9 @@ import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
*******************************************************************************/
public class RunAssociatedScriptAction
{
private Map<AssociatedScriptCodeReference, ScriptRevision> scriptRevisionCache = new HashMap<>();
/*******************************************************************************
**
@ -54,32 +56,14 @@ public class RunAssociatedScriptAction
{
ActionHelper.validateSession(input);
Serializable scriptId = getScriptId(input);
if(scriptId == null)
ScriptRevision scriptRevision = getScriptRevision(input);
ExecuteCodeInput executeCodeInput = ExecuteCodeAction.setupExecuteCodeInput(input, scriptRevision);
if(input.getAssociatedScriptContextPrimerInterface() != null)
{
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
input.getAssociatedScriptContextPrimerInterface().primeContext(executeCodeInput, scriptRevision);
}
Script script = getScript(input, scriptId);
if(script.getCurrentScriptRevisionId() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] (scriptId=" + scriptId + ") does not have a current version."));
}
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
executeCodeInput.setSession(input.getSession());
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
executeCodeInput.setContext(new HashMap<>());
if(input.getOutputObject() != null)
{
executeCodeInput.getContext().put("output", input.getOutputObject());
}
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
executeCodeInput.setExecutionLogger(new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
@ -88,15 +72,45 @@ public class RunAssociatedScriptAction
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getScriptRevision(RunAssociatedScriptInput input) throws QException
{
if(!scriptRevisionCache.containsKey(input.getCodeReference()))
{
Serializable scriptId = getScriptId(input);
if(scriptId == null)
{
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
}
Script script = getScript(input, scriptId);
if(script.getCurrentScriptRevisionId() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] (scriptId=" + scriptId + ") does not have a current version."));
}
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
scriptRevisionCache.put(input.getCodeReference(), scriptRevision);
}
return scriptRevisionCache.get(input.getCodeReference());
}
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("scriptRevision");
getInput.setPrimaryKey(scriptRevisionId);
getInput.setIncludeAssociations(true);
GetOutput getOutput = new GetAction().execute(getInput);
if(getOutput.getRecord() == null)
{
@ -114,8 +128,7 @@ public class RunAssociatedScriptAction
*******************************************************************************/
private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(scriptId);
GetOutput getOutput = new GetAction().execute(getInput);
@ -136,8 +149,7 @@ public class RunAssociatedScriptAction
*******************************************************************************/
private Serializable getScriptId(RunAssociatedScriptInput input) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName(input.getCodeReference().getRecordTable());
getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey());
GetOutput getOutput = new GetAction().execute(getInput);

View File

@ -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;
@ -83,8 +86,7 @@ public class StoreAssociatedScriptAction
/////////////////////////////////////////////////////////////
QRecord associatedRecord;
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName(input.getTableName());
getInput.setPrimaryKey(input.getRecordPrimaryKey());
getInput.setShouldGenerateDisplayValues(true);
@ -107,8 +109,7 @@ public class StoreAssociatedScriptAction
////////////////////////////////////////////////////////////////////
// get the script type - that'll be part of the new script's name //
////////////////////////////////////////////////////////////////////
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("scriptType");
getInput.setPrimaryKey(associatedScript.getScriptTypeId());
getInput.setShouldGenerateDisplayValues(true);
@ -125,8 +126,7 @@ public class StoreAssociatedScriptAction
script = new QRecord();
script.setValue("scriptTypeId", associatedScript.getScriptTypeId());
script.setValue("name", associatedRecord.getRecordLabel() + " - " + scriptType.getRecordLabel());
InsertInput insertInput = new InsertInput(input.getInstance());
insertInput.setSession(input.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName("script");
insertInput.setRecords(List.of(script));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -135,8 +135,7 @@ public class StoreAssociatedScriptAction
/////////////////////////////////////////////////////////////
// update the associated record to point at the new script //
/////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput(input.getInstance());
updateInput.setSession(input.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(input.getTableName());
updateInput.setRecords(List.of(new QRecord()
.withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField()))
@ -149,21 +148,18 @@ public class StoreAssociatedScriptAction
////////////////////////////////////////
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(existingScriptId);
GetOutput getOutput = new GetAction().execute(getInput);
script = getOutput.getRecord();
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName("scriptRevision");
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
);
queryInput.setLimit(1);
.withLimit(1));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(!queryOutput.getRecords().isEmpty())
{
@ -187,36 +183,19 @@ public class StoreAssociatedScriptAction
}
}
QRecord scriptRevision = new QRecord()
.withValue("scriptId", script.getValue("id"))
.withValue("contents", input.getCode())
.withValue("commitMessage", commitMessage)
.withValue("sequenceNo", nextSequenceNo);
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);
try
{
scriptRevision.setValue("author", input.getSession().getUser().getFullName());
}
catch(Exception e)
{
scriptRevision.setValue("author", "Unknown");
}
InsertInput insertInput = new InsertInput(input.getInstance());
insertInput.setSession(input.getSession());
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
scriptRevision = insertOutput.getRecords().get(0);
////////////////////////////////////////////////////
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput(input.getInstance());
updateInput.setSession(input.getSession());
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);
output.setScriptId(script.getValueInteger("id"));
output.setScriptName(script.getValueString("name"));
output.setScriptRevisionId(storeScriptRevisionOutput.getValueInteger("scriptRevisionId"));
output.setScriptRevisionSequenceNo(storeScriptRevisionOutput.getValueInteger("scriptRevisionSequenceNo"));
}
}

View File

@ -1,65 +0,0 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.util.HashMap;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Class for running a test of a script - e.g., maybe before it is saved.
*******************************************************************************/
public class TestScriptAction
{
/*******************************************************************************
**
*******************************************************************************/
public void run(TestScriptInput input, TestScriptOutput output) throws QException
{
QTableMetaData table = input.getTable();
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
executeCodeInput.setSession(input.getSession());
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
executeCodeInput.setContext(new HashMap<>());
// todo! if(input.getOutputObject() != null)
// todo! {
// todo! executeCodeInput.getContext().put("output", input.getOutputObject());
// todo! }
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(input.getCode()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
executeCodeInput.setExecutionLogger(new BuildScriptLogAndScriptLogLineExecutionLogger());
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
// todo! output.setOutput(executeCodeOutput.getOutput());
}
}

View File

@ -0,0 +1,121 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.BuildScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeOutput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptInput;
import com.kingsrook.qqq.backend.core.model.actions.scripts.TestScriptOutput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
/*******************************************************************************
** Interface to be implemented by script-running actions, if they want to allow
** themselves to be used for user-testing of their script.
*******************************************************************************/
public interface TestScriptActionInterface
{
/*******************************************************************************
** Called to adapt or translate data from the TestScriptInput (which would just
** have a map of name-value pairs) to the actual input object(s) used by the script.
**
** Note - such a method may want or need to put an "output" object into the
** executeCodeInput's context map.
*******************************************************************************/
void setupTestScriptInput(TestScriptInput testScriptInput, ExecuteCodeInput executeCodeInput) throws QException;
/*******************************************************************************
** Called to adapt or translate the output object of the script execution to
** something suitable for returning to the caller.
**
** Default implementation may always be suitable?
*******************************************************************************/
default Serializable processTestScriptOutput(ExecuteCodeOutput executeCodeOutput)
{
return (executeCodeOutput.getOutput());
}
/*******************************************************************************
** Define the list of input fields for testing the script. The names of these
** fields will end up as keys in the setupTestScriptInput method's testScriptInput object.
*******************************************************************************/
List<QFieldMetaData> getTestInputFields();
/*******************************************************************************
** Define the list of output fields when testing the script. The output object
** returned from processTestScriptOutput should have keys that match these field names.
*******************************************************************************/
List<QFieldMetaData> getTestOutputFields();
/*******************************************************************************
** Execute a test script.
*******************************************************************************/
default void execute(TestScriptInput input, TestScriptOutput output) throws QException
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// todo - could this be merged with the various other script runners, to use ExecuteCodeAction.setupExecuteCodeInput?? //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setContext(new HashMap<>());
executeCodeInput.setCodeReference(input.getCodeReference());
BuildScriptLogAndScriptLogLineExecutionLogger executionLogger = new BuildScriptLogAndScriptLogLineExecutionLogger(null, null);
executeCodeInput.setExecutionLogger(executionLogger);
try
{
setupTestScriptInput(input, executeCodeInput);
ScriptRevision scriptRevision = new ScriptRevision().withApiName(input.getApiName()).withApiVersion(input.getApiVersion());
if(this instanceof AssociatedScriptContextPrimerInterface associatedScriptContextPrimerInterface)
{
associatedScriptContextPrimerInterface.primeContext(executeCodeInput, scriptRevision);
}
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
ExecuteCodeAction.addApiUtilityToContext(executeCodeInput.getContext(), scriptRevision);
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutputObject(processTestScriptOutput(executeCodeOutput));
}
catch(Exception e)
{
output.setException(e);
}
output.setScriptLog(executionLogger.getScriptLog());
output.setScriptLogLines(executionLogger.getScriptLogLines());
}
}

View File

@ -0,0 +1,160 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
**
*******************************************************************************/
public class AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger implements ScriptExecutionLoggerInterface
{
private static final QLogger LOG = QLogger.getLogger(AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger.class);
private List<QRecord> scriptLogs = new ArrayList<>();
private List<List<QRecord>> scriptLogLines = new ArrayList<>();
/*******************************************************************************
**
*******************************************************************************/
public AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger()
{
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionStart(ExecuteCodeInput executeCodeInput)
{
super.acceptExecutionStart(executeCodeInput);
super.setScriptLogLines(new ArrayList<>());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptException(Exception exception)
{
accumulate(null, exception);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void acceptExecutionEnd(Serializable output)
{
accumulate(output, null);
}
/*******************************************************************************
**
*******************************************************************************/
private void accumulate(Serializable output, Exception exception)
{
super.updateHeaderAtEnd(output, exception);
scriptLogs.add(super.getScriptLog());
scriptLogLines.add(new ArrayList<>(super.getScriptLogLines()));
super.getScriptLogLines().clear();
}
/*******************************************************************************
**
*******************************************************************************/
public void storeAndClear()
{
try
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptLog");
insertInput.setRecords(scriptLogs);
InsertOutput insertOutput = new InsertAction().execute(insertInput);
List<QRecord> flatScriptLogLines = new ArrayList<>();
for(int i = 0; i < insertOutput.getRecords().size(); i++)
{
QRecord insertedScriptLog = insertOutput.getRecords().get(i);
List<QRecord> subScriptLogLines = scriptLogLines.get(i);
subScriptLogLines.forEach(r -> r.setValue("scriptLogId", insertedScriptLog.getValueInteger("id")));
flatScriptLogLines.addAll(subScriptLogLines);
}
insertInput = new InsertInput();
insertInput.setTableName("scriptLogLine");
insertInput.setRecords(flatScriptLogLines);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.warn("Error storing script logs", e);
}
scriptLogs.clear();
scriptLogLines.clear();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptId(Integer scriptId)
{
super.setScriptId(scriptId);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptRevisionId(Integer scriptRevisionId)
{
super.setScriptRevisionId(scriptRevisionId);
}
}

View File

@ -27,12 +27,11 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -40,9 +39,9 @@ import org.apache.logging.log4j.Logger;
** scriptLogLine records - but doesn't insert them. e.g., useful for testing
** (both in junit, and for users in-app).
*******************************************************************************/
public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface
public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecutionLoggerInterface, ScriptExecutionLoggerInterface
{
private static final Logger LOG = LogManager.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
private static final QLogger LOG = QLogger.getLogger(BuildScriptLogAndScriptLogLineExecutionLogger.class);
private QRecord scriptLog;
private List<QRecord> scriptLogLines = new ArrayList<>();
@ -218,4 +217,37 @@ public class BuildScriptLogAndScriptLogLineExecutionLogger implements QCodeExecu
{
this.scriptLog = scriptLog;
}
/*******************************************************************************
** Setter for scriptLogLines
**
*******************************************************************************/
protected void setScriptLogLines(List<QRecord> scriptLogLines)
{
this.scriptLogLines = scriptLogLines;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptId(Integer scriptId)
{
this.scriptId = scriptId;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public void setScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
}
}

View File

@ -24,12 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -37,11 +36,12 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
private static final Logger LOG = LogManager.getLogger(Log4jCodeExecutionLogger.class);
private static final QLogger LOG = QLogger.getLogger(Log4jCodeExecutionLogger.class);
private QCodeReference qCodeReference;
private String uuid = UUID.randomUUID().toString();
private boolean includeUUID = true;
/*******************************************************************************
@ -53,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);
}
@ -64,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);
}
@ -75,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);
}
@ -87,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,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.scripts.logging;
/*******************************************************************************
**
*******************************************************************************/
public interface ScriptExecutionLoggerInterface
{
/*******************************************************************************
**
*******************************************************************************/
void setScriptId(Integer scriptId);
/*******************************************************************************
**
*******************************************************************************/
void setScriptRevisionId(Integer scriptRevisionId);
}

View File

@ -26,13 +26,12 @@ import java.io.Serializable;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -41,7 +40,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLogAndScriptLogLineExecutionLogger
{
private static final Logger LOG = LogManager.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
private static final QLogger LOG = QLogger.getLogger(StoreScriptLogAndScriptLogLineExecutionLogger.class);
@ -66,8 +65,7 @@ public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLo
{
super.acceptExecutionStart(executeCodeInput);
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
insertInput.setSession(executeCodeInput.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptLog");
insertInput.setRecords(List.of(getScriptLog()));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -112,16 +110,14 @@ public class StoreScriptLogAndScriptLogLineExecutionLogger extends BuildScriptLo
try
{
updateHeaderAtEnd(output, exception);
UpdateInput updateInput = new UpdateInput(executeCodeInput.getInstance());
updateInput.setSession(executeCodeInput.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("scriptLog");
updateInput.setRecords(List.of(getScriptLog()));
new UpdateAction().execute(updateInput);
if(CollectionUtils.nullSafeHasContents(getScriptLogLines()))
{
InsertInput insertInput = new InsertInput(executeCodeInput.getInstance());
insertInput.setSession(executeCodeInput.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptLogLine");
insertInput.setRecords(getScriptLogLines());
new InsertAction().execute(insertInput);

View File

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

@ -0,0 +1,90 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables;
import 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;
/*******************************************************************************
** Action to run an aggregate against a table.
**
*******************************************************************************/
public class AggregateAction
{
private static final QLogger LOG = QLogger.getLogger(AggregateAction.class);
private AggregateInterface aggregateInterface;
/*******************************************************************************
**
*******************************************************************************/
public AggregateOutput execute(AggregateInput aggregateInput) throws QException
{
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());
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

@ -24,19 +24,46 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostDeleteCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCustomizer;
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;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.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.statusmessages.NotFoundStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -45,7 +72,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class DeleteAction
{
private static final Logger LOG = LogManager.getLogger(DeleteAction.class);
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
@ -56,21 +83,50 @@ public class DeleteAction
{
ActionHelper.validateSession(deleteInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
// todo pre-customization - just get to modify the request?
QTableMetaData table = deleteInput.getTable();
String primaryKeyFieldName = table.getPrimaryKeyField();
QFieldMetaData primaryKeyField = table.getField(primaryKeyFieldName);
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
List<Serializable> primaryKeys = deleteInput.getPrimaryKeys();
List<Serializable> originalPrimaryKeys = primaryKeys == null ? null : new ArrayList<>(primaryKeys);
if(CollectionUtils.nullSafeHasContents(primaryKeys) && deleteInput.getQueryFilter() != null)
{
throw (new QException("A delete request may not contain both a list of primary keys and a query filter."));
}
DeleteInterface deleteInterface = qModule.getDeleteInterface();
if(deleteInput.getQueryFilter() != null && !deleteInterface.supportsQueryFilterInput())
////////////////////////////////////////////////////////
// make sure the primary keys are of the correct type //
////////////////////////////////////////////////////////
if(CollectionUtils.nullSafeHasContents(primaryKeys))
{
LOG.info("Querying for primary keys, for backend module " + qModule.getBackendType() + " which does not support queryFilter input for deletes");
for(int i = 0; i < primaryKeys.size(); i++)
{
Serializable primaryKey = primaryKeys.get(i);
Serializable valueAsFieldType = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKey);
if(!Objects.equals(primaryKey, valueAsFieldType))
{
primaryKeys.set(i, valueAsFieldType);
}
}
}
//////////////////////////////////////////////////////
// load the backend module and its delete interface //
//////////////////////////////////////////////////////
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
DeleteInterface deleteInterface = qModule.getDeleteInterface();
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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() || CollectionUtils.nullSafeHasContents(table.getAssociations())))
{
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())
{
@ -82,10 +138,348 @@ public class DeleteAction
}
}
DeleteOutput deleteResult = deleteInterface.execute(deleteInput);
// todo post-customization - can do whatever w/ the result if you want
////////////////////////////////////////////////////////////////////////////////
// fetch the old list of records (if the backend supports it), for audits, //
// for "not-found detection", and for the pre-action to use (if there is one) //
////////////////////////////////////////////////////////////////////////////////
Optional<List<QRecord>> oldRecordList = fetchOldRecords(deleteInput, deleteInterface);
return deleteResult;
List<QRecord> customizerResult = performValidations(deleteInput, oldRecordList, false);
List<QRecord> recordsWithValidationErrors = new ArrayList<>();
Map<Serializable, QRecord> recordsWithValidationWarnings = new LinkedHashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if any records got errors in the customizer - if so, remove them from the input list of pkeys to delete //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(customizerResult != null)
{
Set<Serializable> primaryKeysToRemoveFromInput = new HashSet<>();
for(QRecord record : customizerResult)
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
recordsWithValidationErrors.add(record);
primaryKeysToRemoveFromInput.add(record.getValue(primaryKeyFieldName));
}
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
{
recordsWithValidationWarnings.put(record.getValue(primaryKeyFieldName), record);
}
}
if(!primaryKeysToRemoveFromInput.isEmpty())
{
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 //
////////////////////////////////////
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// reset the input's list of primary keys -- callers may use & expect that to be what they had passed in!! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
deleteInput.setPrimaryKeys(originalPrimaryKeys);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// merge the backend's output with any validation errors we found (whose pkeys wouldn't have gotten into the backend delete) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> outputRecordsWithErrors = Objects.requireNonNullElseGet(deleteOutput.getRecordsWithErrors(), () -> new ArrayList<>());
outputRecordsWithErrors.addAll(recordsWithValidationErrors);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
}
///////////////////////////////////////////////////////////////////////////////////////////
// combine the warning list from validation to that from execution - avoiding duplicates //
// use a map to manage this list for the rest of this method //
///////////////////////////////////////////////////////////////////////////////////////////
Map<Serializable, QRecord> outputRecordsWithWarningMap = CollectionUtils.nullSafeIsEmpty(deleteOutput.getRecordsWithWarnings()) ? new LinkedHashMap<>()
: deleteOutput.getRecordsWithWarnings().stream().collect(Collectors.toMap(r -> r.getValue(primaryKeyFieldName), r -> r, (a, b) -> a, () -> new LinkedHashMap<>()));
for(Map.Entry<Serializable, QRecord> entry : recordsWithValidationWarnings.entrySet())
{
if(!outputRecordsWithWarningMap.containsKey(entry.getKey()))
{
outputRecordsWithWarningMap.put(entry.getKey(), entry.getValue());
}
}
////////////////////////////////////////
// delete associations, if applicable //
////////////////////////////////////////
manageAssociations(primaryKeysWithoutErrors, deleteInput);
//////////////////
// 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 //
//////////////////////////////////////////////////////////////
Optional<AbstractPostDeleteCustomizer> postDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPostDeleteCustomizer.class, table, TableCustomizers.POST_DELETE_RECORD.getRole());
if(postDeleteCustomizer.isPresent() && oldRecordList.isPresent())
{
////////////////////////////////////////////////////////////////////////////
// make list of records that are still good - to pass into the customizer //
////////////////////////////////////////////////////////////////////////////
List<QRecord> recordsForCustomizer = makeListOfRecordsNotInErrorList(primaryKeyFieldName, oldRecordList.get(), outputRecordsWithErrors);
try
{
postDeleteCustomizer.get().setDeleteInput(deleteInput);
List<QRecord> postCustomizerResult = postDeleteCustomizer.get().apply(recordsForCustomizer);
///////////////////////////////////////////////////////
// check if any records got errors in the customizer //
///////////////////////////////////////////////////////
for(QRecord record : postCustomizerResult)
{
Serializable pkey = record.getValue(primaryKeyFieldName);
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
outputRecordsWithErrors.add(record);
outputRecordsWithWarningMap.remove(pkey);
}
else if(CollectionUtils.nullSafeHasContents(record.getWarnings()))
{
outputRecordsWithWarningMap.put(pkey, record);
}
}
}
catch(Exception e)
{
for(QRecord record : recordsForCustomizer)
{
record.addWarning(new QWarningMessage("An error occurred after the delete: " + e.getMessage()));
outputRecordsWithWarningMap.put(record.getValue(primaryKeyFieldName), record);
}
}
}
deleteOutput.setRecordsWithErrors(outputRecordsWithErrors);
deleteOutput.setRecordsWithWarnings(new ArrayList<>(outputRecordsWithWarningMap.values()));
return deleteOutput;
}
/*******************************************************************************
** this method takes in the deleteInput, and the list of old records that matched
** the pkeys in that input.
**
** it'll check if any of those pkeys aren't found (in a sub-method) - a record
** with an error message will be added to oldRecordList for any such records.
**
** it'll also then call the pre-customizer, if there is one - taking in the
** oldRecordList. it can add other errors or warnings to records.
**
** The return value here is basically oldRecordList - possibly with some new
** entries for the pkey-not-founds, and possibly w/ errors and warnings from the
** customizer.
*******************************************************************************/
public List<QRecord> performValidations(DeleteInput deleteInput, Optional<List<QRecord>> oldRecordList, boolean isPreview) throws QException
{
if(oldRecordList.isEmpty())
{
return (null);
}
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 //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreDeleteCustomizer> preDeleteCustomizer = QCodeLoader.getTableCustomizer(AbstractPreDeleteCustomizer.class, table, TableCustomizers.PRE_DELETE_RECORD.getRole());
List<QRecord> customizerResult = oldRecordList.get();
if(preDeleteCustomizer.isPresent())
{
preDeleteCustomizer.get().setDeleteInput(deleteInput);
preDeleteCustomizer.get().setIsPreview(isPreview);
customizerResult = preDeleteCustomizer.get().apply(oldRecordList.get());
}
/////////////////////////////////////////////////////////////////////////
// add any pkey-not-found records to the front of the customizerResult //
/////////////////////////////////////////////////////////////////////////
customizerResult.addAll(primaryKeysNotFound);
return customizerResult;
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> makeListOfRecordsNotInErrorList(String primaryKeyField, List<QRecord> oldRecordList, List<QRecord> outputRecordsWithErrors)
{
Map<Serializable, QRecord> recordsWithErrorsMap = outputRecordsWithErrors.stream().collect(Collectors.toMap(r -> r.getValue(primaryKeyField), r -> r));
List<QRecord> recordsForCustomizer = new ArrayList<>();
for(QRecord record : oldRecordList)
{
if(!recordsWithErrorsMap.containsKey(record.getValue(primaryKeyField)))
{
recordsForCustomizer.add(record);
}
}
return recordsForCustomizer;
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(Set<Serializable> primaryKeysWithoutErrors, DeleteInput deleteInput) throws QException
{
QTableMetaData table = deleteInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// e.g., order -> orderLine
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
QQueryFilter filter = new QQueryFilter();
if(join.getJoinOns().size() == 1 && join.getJoinOns().get(0).getLeftField().equals(table.getPrimaryKeyField()))
{
filter.addCriteria(new QFilterCriteria(join.getJoinOns().get(0).getRightField(), QCriteriaOperator.IN, new ArrayList<>(primaryKeysWithoutErrors)));
}
else
{
throw (new QException("Join of this type is not supported for an associated delete at this time..."));
}
QTableMetaData associatedTable = QContext.getQInstance().getTable(association.getAssociatedTableName());
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(association.getAssociatedTableName());
queryInput.setFilter(filter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
List<Serializable> associatedKeys = queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).toList();
if(CollectionUtils.nullSafeHasContents(associatedKeys))
{
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);
DeleteOutput nextLevelDeleteOutput = new DeleteAction().execute(nextLevelDeleteInput);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Optional<List<QRecord>> fetchOldRecords(DeleteInput deleteInput, DeleteInterface deleteInterface) throws QException
{
if(deleteInterface.supportsPreFetchQuery())
{
List<Serializable> primaryKeyList = deleteInput.getPrimaryKeys();
if(CollectionUtils.nullSafeIsEmpty(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
{
primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
}
if(CollectionUtils.nullSafeHasContents(primaryKeyList))
{
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(deleteInput.getTableName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(deleteInput.getTable().getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeyList)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
return (Optional.of(queryOutput.getRecords()));
}
}
return (Optional.empty());
}
/*******************************************************************************
** Note - the "can be accessed" part of this method name - it implies that
** records that you can't see because of security - that they won't be found
** by the query here, so it's the same to you as if they don't exist at all!
**
** If this method identifies any missing records (e.g., from PKeys that are
** requested to be deleted, but don't exist (or can't be seen)), then it will
** return those as new QRecords, with error messages.
*******************************************************************************/
private List<QRecord> validateRecordsExistAndCanBeAccessed(DeleteInput deleteInput, List<QRecord> oldRecordList) throws QException
{
List<QRecord> recordsWithErrors = new ArrayList<>();
QTableMetaData table = deleteInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
List<List<Serializable>> pages = CollectionUtils.getPages(deleteInput.getPrimaryKeys(), 1000);
for(List<Serializable> page : pages)
{
Map<Serializable, QRecord> oldRecordMapByPrimaryKey = new HashMap<>();
for(QRecord record : oldRecordList)
{
Serializable primaryKeyValue = record.getValue(table.getPrimaryKeyField());
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
oldRecordMapByPrimaryKey.put(primaryKeyValue, record);
}
for(Serializable primaryKeyValue : page)
{
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
if(!oldRecordMapByPrimaryKey.containsKey(primaryKeyValue))
{
QRecord recordWithError = new QRecord();
recordsWithErrors.add(recordWithError);
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
recordWithError.addError(new NotFoundStatusMessage("No record was found to delete for " + primaryKeyField.getLabel() + " = " + primaryKeyValue));
}
}
}
return (recordsWithErrors);
}
@ -102,7 +496,8 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
QueryInput queryInput = new QueryInput(deleteInput.getInstance(), deleteInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(deleteInput.getTableName());
queryInput.setFilter(deleteInput.getQueryFilter());
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);

View File

@ -22,15 +22,19 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.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.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
@ -40,6 +44,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.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.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
@ -50,14 +57,23 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class GetAction
{
private Optional<Function<QRecord, QRecord>> postGetRecordCustomizer;
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
private GetInput getInput;
private QValueFormatter qValueFormatter;
private QPossibleValueTranslator qPossibleValueTranslator;
/*******************************************************************************
**
*******************************************************************************/
public QRecord executeForRecord(GetInput getInput) throws QException
{
return (execute(getInput).getRecord());
}
/*******************************************************************************
**
*******************************************************************************/
@ -65,7 +81,13 @@ public class GetAction
{
ActionHelper.validateSession(getInput);
postGetRecordCustomizer = QCodeLoader.getTableCustomizerFunction(getInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
QTableMetaData table = getInput.getTable();
if(table == null)
{
throw (new QException("Requested to Get a record from an unrecognized table: " + getInput.getTableName()));
}
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.getInput = getInput;
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
@ -79,22 +101,31 @@ public class GetAction
}
catch(IllegalStateException ise)
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if a module doesn't implement Get directly - try to do a Get by a Query by the primary key //
// see below. //
////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if a module doesn't implement Get directly - try to do a Get by a Query in the DefaultGetInterface (inner class) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
GetOutput getOutput;
if(getInterface != null)
if(getInterface == null)
{
getOutput = getInterface.execute(getInput);
}
else
{
getOutput = performGetViaQuery(getInput);
getInterface = new DefaultGetInterface();
}
getInterface.validateInput(getInput);
getOutput = getInterface.execute(getInput);
////////////////////////////
// handle cache use-cases //
////////////////////////////
if(table.getCacheOf() != null)
{
new GetActionCacheHelper().handleCaching(getInput, getOutput);
}
////////////////////////////////////////////////////////
// if the record is found, perform post-actions on it //
////////////////////////////////////////////////////////
if(getOutput.getRecord() != null)
{
getOutput.setRecord(postRecordActions(getOutput.getRecord()));
@ -105,23 +136,78 @@ 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).
*******************************************************************************/
public GetOutput executeViaQuery(GetInput getInput) throws QException
{
return (new DefaultGetInterface().execute(getInput));
}
/*******************************************************************************
**
*******************************************************************************/
private GetOutput performGetViaQuery(GetInput getInput) throws QException
private static class DefaultGetInterface implements GetInterface
{
QueryInput queryInput = new QueryInput(getInput.getInstance());
queryInput.setSession(getInput.getSession());
queryInput.setTableName(getInput.getTableName());
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(getInput.getTable().getPrimaryKeyField(), QCriteriaOperator.EQUALS, List.of(getInput.getPrimaryKey()))));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
GetOutput getOutput = new GetOutput();
if(!queryOutput.getRecords().isEmpty())
@Override
public GetOutput execute(GetInput getInput) throws QException
{
getOutput.setRecord(queryOutput.getRecords().get(0));
QueryInput queryInput = convertGetInputToQueryInput(getInput);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
GetOutput getOutput = new GetOutput();
if(!queryOutput.getRecords().isEmpty())
{
getOutput.setRecord(queryOutput.getRecords().get(0));
}
return (getOutput);
}
return (getOutput);
}
/*******************************************************************************
**
*******************************************************************************/
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;
}
@ -135,7 +221,7 @@ public class GetAction
QRecord returnRecord = record;
if(this.postGetRecordCustomizer.isPresent())
{
returnRecord = postGetRecordCustomizer.get().apply(record);
returnRecord = postGetRecordCustomizer.get().apply(List.of(record)).get(0);
}
if(getInput.getShouldTranslatePossibleValues())
@ -149,13 +235,39 @@ public class GetAction
if(getInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
QValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
}
if(getInput.getShouldOmitHiddenFields() || getInput.getShouldMaskPasswords())
{
Map<String, QFieldMetaData> fields = QContext.getQInstance().getTable(getInput.getTableName()).getFields();
for(String fieldName : fields.keySet())
{
QFieldMetaData field = fields.get(fieldName);
if(getInput.getShouldOmitHiddenFields() && field.getIsHidden())
{
returnRecord.removeValue(fieldName);
}
else if(getInput.getShouldMaskPasswords() && field.getType() != null && field.getType().needsMasked() && !field.hasAdornmentType(AdornmentType.REVEAL))
{
//////////////////////////////////////////////////////////////////////
// empty out the value completely first (which will remove from //
// display fields as well) then update display value if flag is set //
//////////////////////////////////////////////////////////////////////
returnRecord.removeValue(fieldName);
returnRecord.setValue(fieldName, "************");
if(getInput.getShouldGenerateDisplayValues())
{
returnRecord.setDisplayValue(fieldName, record.getValueString(fieldName));
}
}
}
}
//////////////////////////////////////////////////////////////////////////////
// note - shouldFetchHeavyFields should be handled by the underlying action //
//////////////////////////////////////////////////////////////////////////////
return (returnRecord);
}
}

View File

@ -22,52 +22,343 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCustomizer;
import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreInsertCustomizer;
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.InsertInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput;
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.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.tables.Association;
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.statusmessages.BadInputStatusMessage;
import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Action to insert one or more records.
**
*******************************************************************************/
public class InsertAction
public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOutput>
{
private static final Logger LOG = LogManager.getLogger(InsertAction.class);
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
/*******************************************************************************
**
*******************************************************************************/
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());
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public InsertOutput execute(InsertInput insertInput) throws QException
{
ActionHelper.validateSession(insertInput);
QTableMetaData table = insertInput.getTable();
if(table == null)
{
throw (new QException("Error: Undefined table: " + insertInput.getTableName()));
}
setAutomationStatusField(insertInput);
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
// todo post-customization - can do whatever w/ the result if you want
//////////////////////////////////////////////////////
// load the backend module and its insert interface //
//////////////////////////////////////////////////////
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
InsertInterface insertInterface = qModule.getInsertInterface();
/////////////////////////////
// run standard validators //
/////////////////////////////
performValidations(insertInput, false);
////////////////////////////////////
// have the backend do the insert //
////////////////////////////////////
InsertOutput insertOutput = insertInterface.execute(insertInput);
//////////////////////////////
// log if there were errors //
//////////////////////////////
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream().map(Object::toString)).toList();
if(CollectionUtils.nullSafeHasContents(errors))
{
LOG.info("Errors in insertAction", logPair("tableName", table.getName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
}
//////////////////////////////////////////////////
// insert any associations in the input records //
//////////////////////////////////////////////////
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
//////////////////
// do the audit //
//////////////////
if(insertInput.getOmitDmlAudit())
{
LOG.debug("Requested to omit DML audit");
}
else
{
new DMLAuditAction().execute(new DMLAuditInput()
.withTableActionInput(insertInput)
.withAuditContext(insertInput.getAuditContext())
.withRecordList(insertOutput.getRecords()));
}
//////////////////////////////////////////////////////////////
// finally, run the post-insert customizer, if there is one //
//////////////////////////////////////////////////////////////
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
if(postInsertCustomizer.isPresent())
{
try
{
postInsertCustomizer.get().setInsertInput(insertInput);
insertOutput.setRecords(postInsertCustomizer.get().apply(insertOutput.getRecords()));
}
catch(Exception e)
{
for(QRecord record : insertOutput.getRecords())
{
record.addWarning(new QWarningMessage("An error occurred after the insert: " + e.getMessage()));
}
}
}
return insertOutput;
}
/*******************************************************************************
**
*******************************************************************************/
public void performValidations(InsertInput insertInput, boolean isPreview) throws QException
{
QTableMetaData table = insertInput.getTable();
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
setErrorsIfUniqueKeyErrors(insertInput, table);
if(insertInput.getInputSource().shouldValidateRequiredFields())
{
validateRequiredFields(insertInput);
}
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-insert customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
Optional<AbstractPreInsertCustomizer> preInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPreInsertCustomizer.class, table, TableCustomizers.PRE_INSERT_RECORD.getRole());
if(preInsertCustomizer.isPresent())
{
preInsertCustomizer.get().setInsertInput(insertInput);
preInsertCustomizer.get().setIsPreview(isPreview);
insertInput.setRecords(preInsertCustomizer.get().apply(insertInput.getRecords()));
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateRequiredFields(InsertInput insertInput)
{
QTableMetaData table = insertInput.getTable();
Set<QFieldMetaData> requiredFields = table.getFields().values().stream()
.filter(f -> f.getIsRequired())
.collect(Collectors.toSet());
if(!requiredFields.isEmpty())
{
for(QRecord record : insertInput.getRecords())
{
for(QFieldMetaData requiredField : requiredFields)
{
if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals("")))
{
record.addError(new BadInputStatusMessage("Missing value in required field: " + requiredField.getLabel()));
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QTableMetaData table, List<QRecord> insertedRecords, QBackendTransaction transaction) throws QException
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// e.g., order -> orderLine
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
List<QRecord> nextLevelInserts = new ArrayList<>();
for(QRecord record : insertedRecords)
{
if(CollectionUtils.nullSafeHasContents(record.getErrors()))
{
continue;
}
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
for(QRecord associatedRecord : CollectionUtils.nonNullList(record.getAssociatedRecords().get(association.getName())))
{
for(JoinOn joinOn : join.getJoinOns())
{
QFieldType type = table.getField(joinOn.getLeftField()).getType();
associatedRecord.setValue(joinOn.getRightField(), ValueUtils.getValueAsFieldType(type, record.getValue(joinOn.getLeftField())));
}
nextLevelInserts.add(associatedRecord);
}
}
}
if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
{
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(transaction);
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void setErrorsIfUniqueKeyErrors(InsertInput insertInput, QTableMetaData table) throws QException
{
if(CollectionUtils.nullSafeHasContents(table.getUniqueKeys()))
{
Map<UniqueKey, Set<List<Serializable>>> keysInThisList = new HashMap<>();
if(insertInput.getSkipUniqueKeyCheck())
{
LOG.debug("Skipping unique key check in " + insertInput.getTableName() + " insert.");
return;
}
////////////////////////////////////////////
// check for any pre-existing unique keys //
////////////////////////////////////////////
Map<UniqueKey, Set<List<Serializable>>> existingKeys = new HashMap<>();
List<UniqueKey> uniqueKeys = CollectionUtils.nonNullList(table.getUniqueKeys());
for(UniqueKey uniqueKey : uniqueKeys)
{
existingKeys.put(uniqueKey, UniqueKeyHelper.getExistingKeys(insertInput.getTransaction(), table, insertInput.getRecords(), uniqueKey).keySet());
}
/////////////////////////////////////
// make sure this map is populated //
/////////////////////////////////////
uniqueKeys.forEach(uk -> keysInThisList.computeIfAbsent(uk, x -> new HashSet<>()));
for(QRecord record : insertInput.getRecords())
{
//////////////////////////////////////////////////////////
// check if this record violates any of the unique keys //
//////////////////////////////////////////////////////////
boolean foundDupe = false;
for(UniqueKey uniqueKey : uniqueKeys)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
if(keyValues.isPresent() && (existingKeys.get(uniqueKey).contains(keyValues.get()) || keysInThisList.get(uniqueKey).contains(keyValues.get())))
{
record.addError(new BadInputStatusMessage("Another record already exists with this " + uniqueKey.getDescription(table)));
foundDupe = true;
break;
}
}
///////////////////////////////////////////////////////////////////////////////
// if this record doesn't violate any uk's, then we can add it to the output //
///////////////////////////////////////////////////////////////////////////////
if(!foundDupe)
{
for(UniqueKey uniqueKey : uniqueKeys)
{
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
keyValues.ifPresent(kv -> keysInThisList.get(uniqueKey).add(kv));
}
}
}
}
}
/*******************************************************************************
** If the table being inserted into uses an automation-status field, populate it now.
*******************************************************************************/
private void setAutomationStatusField(InsertInput insertInput)
{
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
RecordAutomationStatusUpdater.setAutomationStatusInRecords(insertInput.getSession(), insertInput.getTable(), insertInput.getRecords(), AutomationStatus.PENDING_INSERT_AUTOMATIONS);
}

View File

@ -22,20 +22,47 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.Set;
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;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
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;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -44,10 +71,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class QueryAction
{
private Optional<Function<QRecord, QRecord>> postQueryRecordCustomizer;
private static final QLogger LOG = QLogger.getLogger(QueryAction.class);
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
private QueryInput queryInput;
private QValueFormatter qValueFormatter;
private QueryInterface queryInterface;
private QPossibleValueTranslator qPossibleValueTranslator;
@ -59,19 +88,58 @@ public class QueryAction
{
ActionHelper.validateSession(queryInput);
postQueryRecordCustomizer = QCodeLoader.getTableCustomizerFunction(queryInput.getTable(), TableCustomizers.POST_QUERY_RECORD.getRole());
if(queryInput.getTableName() == null)
{
throw (new QException("Table name was not specified in query input"));
}
QTableMetaData table = queryInput.getTable();
if(table == null)
{
throw (new QException("A table named [" + queryInput.getTableName() + "] was not found in the active QInstance"));
}
QBackendMetaData backend = queryInput.getBackend();
postQueryRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.queryInput = queryInput;
if(queryInput.getRecordPipe() != null)
{
queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions);
if(queryInput.getIncludeAssociations())
{
//////////////////////////////////////////////////////////////////////////////////////////
// if the user requested to include associations, it's important that that is buffered, //
// (for performance reasons), so, wrap the user's pipe with a buffer //
//////////////////////////////////////////////////////////////////////////////////////////
queryInput.setRecordPipe(new RecordPipeBufferedWrapper(queryInput.getRecordPipe()));
}
}
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)
{
bufferedRecordPipe.finalFlush();
}
if(queryInput.getRecordPipe() == null)
{
@ -83,16 +151,120 @@ public class QueryAction
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QueryInput queryInput, List<QRecord> queryOutputRecords) throws QException
{
QTableMetaData table = queryInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
if(queryInput.getAssociationNamesToInclude() == null || queryInput.getAssociationNamesToInclude().contains(association.getName()))
{
// e.g., order -> orderLine
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
QueryInput nextLevelQueryInput = new QueryInput();
nextLevelQueryInput.setTableName(association.getAssociatedTableName());
nextLevelQueryInput.setIncludeAssociations(true);
nextLevelQueryInput.setAssociationNamesToInclude(buildNextLevelAssociationNamesToInclude(association.getName(), queryInput.getAssociationNamesToInclude()));
QQueryFilter filter = new QQueryFilter();
nextLevelQueryInput.setFilter(filter);
ListingHash<List<Serializable>, QRecord> outerResultMap = new ListingHash<>();
if(join.getJoinOns().size() == 1)
{
JoinOn joinOn = join.getJoinOns().get(0);
Set<Serializable> values = new HashSet<>();
for(QRecord record : queryOutputRecords)
{
Serializable value = record.getValue(joinOn.getLeftField());
Serializable valueAsType = ValueUtils.getValueAsFieldType(table.getField(joinOn.getLeftField()).getType(), value);
values.add(valueAsType);
outerResultMap.add(List.of(valueAsType), record);
}
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.IN, new ArrayList<>(values)));
}
else
{
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QRecord record : queryOutputRecords)
{
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
List<Serializable> values = new ArrayList<>();
for(JoinOn joinOn : join.getJoinOns())
{
Serializable value = record.getValue(joinOn.getLeftField());
values.add(value);
subFilter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, value));
}
outerResultMap.add(values, record);
}
}
QueryOutput nextLevelQueryOutput = new QueryAction().execute(nextLevelQueryInput);
for(QRecord record : nextLevelQueryOutput.getRecords())
{
List<Serializable> values = new ArrayList<>();
for(JoinOn joinOn : join.getJoinOns())
{
Serializable value = record.getValue(joinOn.getRightField());
values.add(value);
}
if(outerResultMap.containsKey(values))
{
for(QRecord outerRecord : outerResultMap.get(values))
{
outerRecord.withAssociatedRecord(association.getName(), record);
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Collection<String> buildNextLevelAssociationNamesToInclude(String name, Collection<String> associationNamesToInclude)
{
if(associationNamesToInclude == null)
{
return (associationNamesToInclude);
}
Set<String> rs = new HashSet<>();
for(String nextLevelCandidateName : associationNamesToInclude)
{
if(nextLevelCandidateName.startsWith(name + "."))
{
rs.add(nextLevelCandidateName.replaceFirst(name + ".", ""));
}
}
return (rs);
}
/*******************************************************************************
** Run the necessary actions on a list of records (which must be a mutable list - e.g.,
** not one created via List.of()). This may include setting display values,
** translating possible values, and running post-record customizations.
*******************************************************************************/
public void postRecordActions(List<QRecord> records)
public void postRecordActions(List<QRecord> records) throws QException
{
if(this.postQueryRecordCustomizer.isPresent())
{
records.replaceAll(t -> postQueryRecordCustomizer.get().apply(t));
records = postQueryRecordCustomizer.get().apply(records);
}
if(queryInput.getShouldTranslatePossibleValues())
@ -101,16 +273,87 @@ public class QueryAction
{
qPossibleValueTranslator = new QPossibleValueTranslator(queryInput.getInstance(), queryInput.getSession());
}
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records);
qPossibleValueTranslator.translatePossibleValuesInRecords(queryInput.getTable(), records, queryInput.getQueryJoins(), queryInput.getFieldsToTranslatePossibleValues());
}
if(queryInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
QValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
if(queryInput.getIncludeAssociations())
{
manageAssociations(queryInput, records);
}
//////////////////////////////
// mask any password fields //
//////////////////////////////
if(queryInput.getShouldOmitHiddenFields() || queryInput.getShouldMaskPasswords())
{
Set<String> maskedFields = new HashSet<>();
Set<String> hiddenFields = new HashSet<>();
//////////////////////////////////////////////////
// build up sets of passwords and hidden fields //
//////////////////////////////////////////////////
Map<String, QFieldMetaData> fields = QContext.getQInstance().getTable(queryInput.getTableName()).getFields();
for(String fieldName : fields.keySet())
{
qValueFormatter = new QValueFormatter();
QFieldMetaData field = fields.get(fieldName);
if(queryInput.getShouldOmitHiddenFields() && field.getIsHidden())
{
hiddenFields.add(fieldName);
}
else if(queryInput.getShouldMaskPasswords() && field.getType() != null && field.getType().needsMasked() && !field.hasAdornmentType(AdornmentType.REVEAL))
{
maskedFields.add(fieldName);
}
}
/////////////////////////////////////////////////////
// iterate over records replacing values with mask //
/////////////////////////////////////////////////////
for(QRecord record : records)
{
/////////////////////////
// clear hidden fields //
/////////////////////////
for(String hiddenFieldName : hiddenFields)
{
record.removeValue(hiddenFieldName);
}
for(String maskedFieldName : maskedFields)
{
//////////////////////////////////////////////////////////////////////
// empty out the value completely first (which will remove from //
// display fields as well) then update display value if flag is set //
//////////////////////////////////////////////////////////////////////
record.removeValue(maskedFieldName);
record.setValue(maskedFieldName, "************");
if(queryInput.getShouldGenerateDisplayValues())
{
record.setDisplayValue(maskedFieldName, record.getValueString(maskedFieldName));
}
}
}
qValueFormatter.setDisplayValuesInRecords(queryInput.getTable(), records);
}
}
/*******************************************************************************
**
*******************************************************************************/
public void cancel()
{
if(queryInterface == null)
{
LOG.warn("queryInterface object was null when requested to cancel");
return;
}
queryInterface.cancelAction();
}
}

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