Compare commits

...

230 Commits

Author SHA1 Message Date
72a9f68de7 Merge branch 'release/0.13.0' 2023-04-20 10:48:45 -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
45c703a6df Update for next development version 2023-01-30 13:31:20 -06:00
309 changed files with 39112 additions and 1162 deletions

View File

@ -1,7 +1,6 @@
version: 2.1
orbs:
slack: circleci/slack@4.10.1
localstack: localstack/platform@1.0
commands:
@ -12,8 +11,10 @@ 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:
@ -24,6 +25,11 @@ commands:
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:
name: Install html2text
command: |
sudo apt-get update
sudo apt-get install -y html2text
mvn_verify:
steps:
@ -59,6 +65,11 @@ commands:
when: always
- store_test_results:
path: ~/test-results
- run:
name: Find Un-tested Classes
command: |
set +o pipefail && 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
when: always
- save_cache:
paths:
- ~/.m2
@ -86,8 +97,6 @@ jobs:
- localstack/startup
- install_java17
- mvn_verify
- slack/notify:
event: fail
mvn_deploy:
executor: localstack/default
@ -96,14 +105,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 +120,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

@ -35,3 +35,4 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

View File

@ -37,13 +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>
<module>qqq-middleware-slack</module>
</modules>
<properties>
<revision>0.11.0</revision>
<revision>0.13.0</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -65,17 +65,22 @@
<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>20220320</version>
<version>20230227</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -95,6 +95,23 @@ public class AsyncJobCallback
/*******************************************************************************
** 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.
*******************************************************************************/

View File

@ -38,6 +38,7 @@ import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -150,7 +151,7 @@ public class AsyncJobManager
asyncJobStatus.setState(AsyncJobState.ERROR);
asyncJobStatus.setCaughtException(e);
getStateProvider().put(uuidAndTypeStateKey, asyncJobStatus);
LOG.warn("Job " + uuidAndTypeStateKey.getUuid() + " ended with an exception: ", e);
LOG.warn("Job ended with an exception", e, logPair("jobId", uuidAndTypeStateKey.getUuid()));
throw (new CompletionException(e));
}
finally

View File

@ -45,9 +45,10 @@ public class AsyncRecordPipeLoop
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;
@ -83,7 +84,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. //
@ -176,4 +177,35 @@ public class AsyncRecordPipeLoop
return (recordCount);
}
/*******************************************************************************
** Getter for minRecordsToConsume
*******************************************************************************/
public Integer getMinRecordsToConsume()
{
return (this.minRecordsToConsume);
}
/*******************************************************************************
** Setter for minRecordsToConsume
*******************************************************************************/
public void setMinRecordsToConsume(Integer minRecordsToConsume)
{
this.minRecordsToConsume = minRecordsToConsume;
}
/*******************************************************************************
** Fluent setter for minRecordsToConsume
*******************************************************************************/
public AsyncRecordPipeLoop withMinRecordsToConsume(Integer minRecordsToConsume)
{
this.minRecordsToConsume = minRecordsToConsume;
return (this);
}
}

View File

@ -25,6 +25,7 @@ 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;
@ -36,6 +37,7 @@ 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;
@ -45,31 +47,78 @@ import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.Pair;
/*******************************************************************************
** Insert an audit (e.g., the same one) against 1 or more records.
** Insert 1 or more audits (and optionally their children, auditDetails)
**
** Takes care of managing the foreign key tables.
** Takes care of managing the foreign key tables (auditTable, auditUser).
**
** Enforces that security key values are provided, if the table has any.
** 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)
{
new AuditAction().execute(new AuditInput()
execute(tableName, recordId, securityKeyValues, message, null);
}
/*******************************************************************************
** 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)
.withRecordIdList(List.of(recordId))
.withRecordId(recordId)
.withSecurityKeyValues(securityKeyValues)
.withMessage(message));
.withMessage(message)
.withDetails(details)
));
}
/*******************************************************************************
** Add 1 auditSingleInput to an AuditInput object - with no details (child records).
*******************************************************************************/
public static AuditInput appendToInput(AuditInput auditInput, String tableName, Integer recordId, Map<String, Serializable> securityKeyValues, String message)
{
return (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)
);
}
@ -81,56 +130,102 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
public AuditOutput execute(AuditInput input)
{
AuditOutput auditOutput = new AuditOutput();
try
if(CollectionUtils.nullSafeHasContents(input.getAuditSingleInputList()))
{
QTableMetaData table = QContext.getQInstance().getTable(input.getAuditTableName());
if(table == null)
try
{
throw (new QException("Requested audit for an unrecognized table name: " + input.getAuditTableName()));
}
List<QRecord> auditRecords = new ArrayList<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
if(input.getSecurityKeyValues() == null || !input.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
for(AuditSingleInput auditSingleInput : CollectionUtils.nonNullList(input.getAuditSingleInputList()))
{
throw (new QException("Missing securityKeyValue [" + recordSecurityLock.getSecurityKeyType() + "] in audit request for table " + input.getAuditTableName()));
}
}
Integer auditTableId = getIdForName("auditTable", input.getAuditTableName());
Integer auditUserId = getIdForName("auditUser", Objects.requireNonNullElse(input.getAuditUserName(), getSessionUserName()));
Instant timestamp = Objects.requireNonNullElse(input.getTimestamp(), Instant.now());
List<QRecord> auditRecords = new ArrayList<>();
for(Integer recordId : input.getRecordIdList())
{
QRecord record = new QRecord()
.withValue("auditTableId", auditTableId)
.withValue("auditUserId", auditUserId)
.withValue("timestamp", timestamp)
.withValue("message", input.getMessage())
.withValue("recordId", recordId);
if(input.getSecurityKeyValues() != null)
{
for(Map.Entry<String, Serializable> entry : input.getSecurityKeyValues().entrySet())
/////////////////////////////////////////
// validate table is known in instance //
/////////////////////////////////////////
QTableMetaData table = QContext.getQInstance().getTable(auditSingleInput.getAuditTableName());
if(table == null)
{
record.setValue(entry.getKey(), entry.getValue());
throw (new QException("Requested audit for an unrecognized table name: " + auditSingleInput.getAuditTableName()));
}
///////////////////////////////////////////////////
// validate security keys on the table are given //
///////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : 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));
}
}
auditRecords.add(record);
insertInput = new InsertInput();
insertInput.setTableName("auditDetail");
insertInput.setRecords(auditDetailRecords);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.error("Error performing an audit", e);
}
}
InsertInput insertInput = new InsertInput();
insertInput.setTableName("audit");
insertInput.setRecords(auditRecords);
new InsertAction().execute(insertInput);
}
catch(Exception e)
{
LOG.error("Error performing an audit", e);
}
return (auditOutput);
}
@ -156,54 +251,64 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
*******************************************************************************/
private Integer getIdForName(String tableName, String nameValue) throws QException
{
Integer id = fetchIdFromName(tableName, nameValue);
if(id != null)
Pair<String, String> key = new Pair<>(tableName, nameValue);
if(!cachedFetches.containsKey(key))
{
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");
Integer id = fetchIdFromName(tableName, nameValue);
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);
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));
}
id = fetchIdFromName(tableName, nameValue);
if(id != null)
{
return id;
}
/////////////
// give up //
/////////////
throw (new QException("Unable to get id for " + tableName + " named " + nameValue));
return (cachedFetches.get(key));
}
@ -211,7 +316,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
/*******************************************************************************
**
*******************************************************************************/
private static Integer fetchIdFromName(String tableName, String nameValue) throws QException
private Integer fetchIdFromName(String tableName, String nameValue) throws QException
{
GetInput getInput = new GetInput();
getInput.setTableName(tableName);
@ -221,7 +326,8 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
{
return (getOutput.getRecord().getValueInteger("id"));
}
return null;
return (null);
}
}

View File

@ -0,0 +1,398 @@
/*
* 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.time.ZoneId;
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.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.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);
/*******************************************************************************
**
*******************************************************************************/
@Override
public DMLAuditOutput execute(DMLAuditInput input) throws QException
{
DMLAuditOutput output = new DMLAuditOutput();
AbstractTableActionInput tableActionInput = input.getTableActionInput();
List<QRecord> recordList = input.getRecordList();
List<QRecord> oldRecordList = input.getOldRecordList();
QTableMetaData table = tableActionInput.getTable();
long start = System.currentTimeMillis();
DMLType dmlType = getDMLType(tableActionInput);
try
{
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 = "";
Optional<AbstractActionInput> actionInput = QContext.getFirstActionInStack();
if(actionInput.isPresent() && actionInput.get() instanceof RunProcessInput runProcessInput)
{
String processName = runProcessInput.getProcessName();
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
if(process != null)
{
contextSuffix = " during process: " + process.getLabel();
}
}
QSession qSession = QContext.getQSession();
String apiVersion = qSession.getValue("apiVersion");
if(apiVersion != null)
{
String apiLabel = qSession.getValue("apiLabel");
if(!StringUtils.hasContent(apiLabel))
{
apiLabel = "API";
}
contextSuffix += (" via " + apiLabel + " Version: " + apiVersion);
}
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), "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(), qSession);
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)
{
if(!record.getValues().containsKey(fieldName))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// if the stored record doesn't have this field name, then don't audit anything about it //
// this is to deal with our Patch style updates not looking like every field was cleared out. //
////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
if(fieldName.equals("modifyDate") || fieldName.equals("createDate") || fieldName.equals("automationStatus"))
{
continue;
}
QFieldMetaData field = table.getField(fieldName);
Serializable value = ValueUtils.getValueAsFieldType(field.getType(), record.getValue(fieldName));
Serializable oldValue = oldRecord == null ? null : ValueUtils.getValueAsFieldType(field.getType(), oldRecord.getValue(fieldName));
QRecord detailRecord = null;
if(oldRecord == null)
{
if(DMLType.INSERT.equals(dmlType) && value == null)
{
continue;
}
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formattedValue);
detailRecord.withValue("newValue", formattedValue);
}
else
{
if(!Objects.equals(oldValue, value))
{
String formattedValue = getFormattedValueForAuditDetail(record, fieldName, field, value);
String formattedOldValue = getFormattedValueForAuditDetail(oldRecord, fieldName, field, oldValue);
if(oldValue == null)
{
detailRecord = new QRecord().withValue("message", "Set " + field.getLabel() + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("newValue", formattedValue);
}
else if(value == null)
{
detailRecord = new QRecord().withValue("message", "Removed " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " from " + field.getLabel());
detailRecord.withValue("oldValue", formattedOldValue);
}
else
{
detailRecord = new QRecord().withValue("message", "Changed " + field.getLabel() + " from " + formatFormattedValueForDetailMessage(field, formattedOldValue) + " to " + formatFormattedValueForDetailMessage(field, formattedValue));
detailRecord.withValue("oldValue", formattedOldValue);
detailRecord.withValue("newValue", formattedValue);
}
}
}
if(detailRecord != null)
{
detailRecord.withValue("fieldName", fieldName);
details.add(detailRecord);
}
}
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), "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.debug("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);
}
/*******************************************************************************
**
*******************************************************************************/
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;
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
{
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
}
return securityKeyValues;
}
/*******************************************************************************
**
*******************************************************************************/
public static AuditLevel getAuditLevel(AbstractTableActionInput tableActionInput)
{
QTableMetaData table = tableActionInput.getTable();
if(table.getAuditRules() == null)
{
return (AuditLevel.NONE);
}
return (table.getAuditRules().getAuditLevel());
}
/*******************************************************************************
**
*******************************************************************************/
private 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

@ -25,10 +25,18 @@ package com.kingsrook.qqq.backend.core.actions.automation;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.tables.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;
@ -119,14 +127,63 @@ public class RecordAutomationStatusUpdater
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);
}
}
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);
}
}
return (false);
return (true);
}
/*******************************************************************************
**
*******************************************************************************/
private static boolean areThereTableTriggersForTable(QTableMetaData table, TriggerEvent triggerEvent)
{
if(QContext.getQInstance().getTable(TableTrigger.TABLE_NAME) == null)
{
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);
}
}
@ -155,6 +212,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,95 @@
/*
* 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.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("currentScriptRevision")));
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,15 +25,16 @@ 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;
@ -49,9 +50,12 @@ 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;
@ -60,6 +64,7 @@ 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 com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
import org.apache.commons.lang.NotImplementedException;
@ -87,6 +92,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
@ -102,51 +112,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));
}
}
@ -178,12 +163,12 @@ public class PollingAutomationPerTableRunner implements Runnable
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)
{
@ -201,14 +186,18 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
*******************************************************************************/
public 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;
}
LOG.info(" Query for records " + automationStatus + " in " + table);
LOG.debug(" Query for records " + automationStatus + " in " + table);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// run an async-pipe loop - that will query for records in PENDING - put them in a pipe - then apply actions to them //
@ -247,6 +236,59 @@ 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())
{
// todo - get filter if there is/was one
rs.add(new TableAutomationAction()
.withName("Script:" + record.getValue("scriptId"))
.withFilter(null)
.withTriggerEvent(triggerEvent)
.withPriority(record.getValueInteger("priority"))
.withCodeReference(new QCodeReference(RunRecordScriptAutomationHandler.class))
.withValues(MapBuilder.of("scriptId", record.getValue("scriptId")))
);
}
}
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
@ -275,12 +317,12 @@ public class PollingAutomationPerTableRunner implements Runnable
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// note - this method - will re-query the objects, so we should have confidence that their data is fresh... //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(session, table, records, action);
LOG.info("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
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.info(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(instance, session, table, matchingQRecords, action);
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(table, matchingQRecords, action);
}
}
catch(Exception e)
@ -317,7 +359,7 @@ 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();
queryInput.setTableName(table.getName());
@ -359,7 +401,7 @@ public class PollingAutomationPerTableRunner implements Runnable
** Finally, actually run action code against a list of known matching records.
** todo not commit - move to somewhere genericer
*******************************************************************************/
public static void applyActionToMatchingRecords(QInstance instance, 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()))
{
@ -381,11 +423,20 @@ 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)
@ -394,6 +445,7 @@ public class PollingAutomationPerTableRunner implements Runnable
RecordAutomationInput input = new RecordAutomationInput();
input.setTableName(table.getName());
input.setRecordList(records);
input.setAction(action);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
recordAutomationHandler.execute(input);

View File

@ -34,6 +34,7 @@ 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 static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -101,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 //
@ -140,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 //
@ -179,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

@ -26,12 +26,14 @@ 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;
@ -53,7 +55,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
protected String openTopLevelBulletList()
public static String openTopLevelBulletList()
{
return ("""
<div style="padding-left: 2rem;">
@ -65,7 +67,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
protected String closeTopLevelBulletList()
public static String closeTopLevelBulletList()
{
return ("""
</ul>
@ -119,7 +121,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableBulkLoad(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + tableName + ".bulkInsert");
}
@ -128,8 +130,14 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableBulkLoadChildren(String tableName) throws QException
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");
}
@ -140,7 +148,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableCreate(RenderWidgetInput input, String tableName) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/create");
}
@ -151,18 +159,71 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkTableCreateWithDefaultValues(RenderWidgetInput input, String tableName, Map<String, Serializable> defaultValues) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
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(RenderWidgetInput input, String tableName, QQueryFilter filter) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (null);
}
return (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
}
@ -173,8 +234,34 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, String tableName, QQueryFilter filter, Integer noOfRecords, String singularLabel, String pluralLabel) throws QException
{
return (aHrefTableFilterNoOfRecords(input, tableName, filter, noOfRecords, singularLabel, pluralLabel, false));
}
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableFilterNoOfRecords(RenderWidgetInput input, 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(input, tableName, filter);
return ("<a href=\"" + href + "\">" + QValueFormatter.formatValue(DisplayFormat.COMMAS, noOfRecords) + " " + StringUtils.plural(noOfRecords, singularLabel, pluralLabel) + "</a>");
if(onlyLinkCount)
{
return ("<a href=\"" + href + "\">" + countString + "</a>" + displayText);
}
else
{
return ("<a href=\"" + href + "\">" + countString + displayText + "</a>");
}
}
@ -184,6 +271,12 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String aHrefViewRecord(RenderWidgetInput input, String tableName, Serializable id, String linkText) throws QException
{
String tablePath = QContext.getQInstance().getTablePath(tableName);
if(tablePath == null)
{
return (linkText);
}
return ("<a href=\"" + linkRecordView(input, tableName, id) + "\">" + linkText + "</a>");
}
@ -194,7 +287,7 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkRecordEdit(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/edit");
}
@ -205,20 +298,48 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
*******************************************************************************/
public static String linkRecordView(AbstractActionInput input, String tableName, Serializable recordId) throws QException
{
String tablePath = input.getInstance().getTablePath(input, tableName);
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 = input.getInstance().getProcess(processName);
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
String tableName = process.getTableName();
String tablePath = input.getInstance().getTablePath(input, tableName);
String tablePath = QContext.getQInstance().getTablePath(tableName);
return (tablePath + "/" + recordId + "/" + processName);
}
@ -227,9 +348,9 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateChild(String childTableName, Map<String, Serializable> defaultValues)
public static String linkTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues) throws QException
{
return (linkTableCreateChild(childTableName, defaultValues, defaultValues.keySet()));
return (linkTableCreateChild(input, childTableName, defaultValues, defaultValues.keySet()));
}
@ -237,9 +358,9 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableCreateChild(String childTableName, Map<String, Serializable> defaultValues)
public static String aHrefTableCreateChild(RenderWidgetInput input, String childTableName, Map<String, Serializable> defaultValues) throws QException
{
return (aHrefTableCreateChild(childTableName, defaultValues, defaultValues.keySet()));
return (aHrefTableCreateChild(input, childTableName, defaultValues, defaultValues.keySet()));
}
@ -247,8 +368,14 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public static String linkTableCreateChild(String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields)
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
@ -261,9 +388,35 @@ public abstract class AbstractHTMLWidgetRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public static String aHrefTableCreateChild(String childTableName, Map<String, Serializable> defaultValues, Set<String> disabledFields)
public static String getChipElement(String icon, String label, String color) throws QException
{
return ("<a href=\"" + linkTableCreateChild(childTableName, defaultValues, defaultValues.keySet()) + "\">Create new</a>");
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

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -122,7 +121,6 @@ public abstract class AbstractWidgetRenderer
//////////////////////////////////////////
Set<String> exists = new HashSet<>();
output.getResults().removeIf(pvs -> !exists.add(pvs.getLabel()));
output.getResults().sort(Comparator.comparing(QPossibleValue::getLabel));
for(QPossibleValue<?> possibleValue : output.getResults())
{
dropdownOptionList.add(Map.of(

View File

@ -53,6 +53,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.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;
@ -117,6 +118,17 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public Builder withMaxRows(Integer maxRows)
{
widgetMetaData.withDefaultValue("maxRows", maxRows);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@ -151,6 +163,12 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
QJoinMetaData join = input.getInstance().getJoin(joinName);
String id = input.getQueryParams().get("id");
Integer maxRows = null;
if(StringUtils.hasContent(input.getQueryParams().get("maxRows")))
{
maxRows = ValueUtils.getValueAsInteger(input.getQueryParams().get("maxRows"));
}
////////////////////////////////////////////////////////
// fetch the record that we're getting children for. //
// e.g., the left-side of the join, with the input id //
@ -182,10 +200,11 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setShouldGenerateDisplayValues(true);
queryInput.setFilter(filter);
queryInput.setLimit(maxRows);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QTableMetaData table = input.getInstance().getTable(join.getRightTable());
String tablePath = input.getInstance().getTablePath(input, table.getName());
String tablePath = input.getInstance().getTablePath(table.getName());
String viewAllLink = tablePath == null ? null : (tablePath + "?filter=" + URLEncoder.encode(JsonUtils.toJson(filter), Charset.defaultCharset()));
ChildRecordListData widgetData = new ChildRecordListData(widgetLabel, queryOutput, table, tablePath, viewAllLink);

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,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,339 @@
/*
* 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 helpIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: blue; position: relative; top: 6px;" aria-hidden="true">help_outline</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String errorIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: red; position: relative; top: 6px;" aria-hidden="true">error_outline</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String warningIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: orange; position: relative; top: 6px;" aria-hidden="true">warning</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String checkIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: green; position: relative; top: 6px;" aria-hidden="true">check</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
public String pendingIcon()
{
return ("""
<span class="material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit" style="color: #0062ff; position: relative; top: 6px;" aria-hidden="true">pending</span>
""");
}
/*******************************************************************************
**
*******************************************************************************/
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(null, 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

@ -29,6 +29,7 @@ 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;
@ -72,9 +73,22 @@ public class PermissionsHelper
private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException
{
warnAboutPermissionSubTypeForTables(permissionSubType);
QTableMetaData table = actionInput.getInstance().getTable(tableName);
QTableMetaData table = QContext.getQInstance().getTable(tableName);
commonCheckPermissionThrowing(getEffectivePermissionRules(table, actionInput.getInstance()), permissionSubType, table.getName(), actionInput);
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));
}
@ -102,7 +116,7 @@ public class PermissionsHelper
*******************************************************************************/
public static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules)
{
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, actionInput.getInstance());
QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, QContext.getQInstance());
String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName());
switch(rules.getLevel())
@ -167,8 +181,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map<String, Serializable> processValues) throws QPermissionDeniedException
{
QProcessMetaData process = actionInput.getInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, actionInput.getInstance());
QProcessMetaData process = QContext.getQInstance().getProcess(processName);
QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, QContext.getQInstance());
if(effectivePermissionRules.getCustomPermissionChecker() != null)
{
@ -180,7 +194,7 @@ public class PermissionsHelper
return;
}
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName(), actionInput);
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName());
}
@ -208,8 +222,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException
{
QAppMetaData app = actionInput.getInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput);
QAppMetaData app = QContext.getQInstance().getApp(appName);
commonCheckPermissionThrowing(getEffectivePermissionRules(app, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName());
}
@ -237,8 +251,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException
{
QReportMetaData report = actionInput.getInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput);
QReportMetaData report = QContext.getQInstance().getReport(reportName);
commonCheckPermissionThrowing(getEffectivePermissionRules(report, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName());
}
@ -266,8 +280,8 @@ public class PermissionsHelper
*******************************************************************************/
public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException
{
QWidgetMetaDataInterface widget = actionInput.getInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput);
QWidgetMetaDataInterface widget = QContext.getQInstance().getWidget(widgetName);
commonCheckPermissionThrowing(getEffectivePermissionRules(widget, QContext.getQInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName());
}
@ -435,7 +449,7 @@ public class PermissionsHelper
}
}
if(hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
if(hasPermission(QContext.getQSession(), permissionBaseName, effectivePermissionSubType))
{
return (PermissionCheckResult.ALLOW);
}
@ -523,7 +537,7 @@ public class PermissionsHelper
/*******************************************************************************
**
*******************************************************************************/
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name, AbstractActionInput actionInput) throws QPermissionDeniedException
private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name) throws QPermissionDeniedException
{
PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType);
String permissionBaseName = getEffectivePermissionBaseName(rules, name);
@ -533,9 +547,9 @@ public class PermissionsHelper
return;
}
if(!hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
if(!hasPermission(QContext.getQSession(), permissionBaseName, effectivePermissionSubType))
{
// LOG.debug("Throwing permission denied for: " + getPermissionName(permissionBaseName, effectivePermissionSubType) + " for " + actionInput.getSession().getUser());
// LOG.debug("Throwing permission denied for: " + getPermissionName(permissionBaseName, effectivePermissionSubType) + " for " + QContext.getQSession().getUser());
throw (new QPermissionDeniedException("Permission denied."));
}
}

View File

@ -31,6 +31,7 @@ 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;
@ -50,7 +51,9 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.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;
@ -60,6 +63,7 @@ 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;
@ -154,6 +158,7 @@ public class RunProcessAction
{
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;
}
@ -231,6 +236,26 @@ 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);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -315,11 +340,22 @@ public class RunProcessAction
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
runBackendStepInput.setStepName(backendStep.getName());
runBackendStepInput.setTableName(process.getTableName());
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 //
///////////////////////////////////////////////////////////////

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

@ -131,6 +131,8 @@ public class SQSQueuePoller implements Runnable
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.addValue("bodies", bodies);
QContext.pushAction(runProcessInput);
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
@ -156,6 +158,10 @@ public class SQSQueuePoller implements Runnable
{
LOG.warn("Error receiving SQS Messages.", e);
}
finally
{
QContext.popAction();
}
}
}
catch(Exception e)

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

@ -52,7 +52,6 @@ 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.JoinsContext;
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;
@ -66,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;
@ -357,7 +357,7 @@ public class GenerateReportAction
////////////////////////////////////////////////
if(transformStep != null)
{
transformStep.postRun(transformStepInput, transformStepOutput);
transformStep.postRun(new BackendStepPostRunInput(transformStepInput), new BackendStepPostRunOutput(transformStepOutput));
}
}
@ -412,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.interpretForObject(valueAsString);
newValues.add(interpretedValue);
}
criterion.setValues(newValues);
}
}
queryFilter.interpretValues(reportInput.getInputValues());
}

View File

@ -0,0 +1,219 @@
/*
* 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.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.ScriptExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.StoreScriptLogAndScriptLogLineExecutionLogger;
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.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.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
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.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())));
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 = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(Objects.requireNonNullElseGet(input.getInputValues(), HashMap::new)));
executeCodeInput.getInput().put("records", new ArrayList<>(input.getRecordList()));
executeCodeInput.setContext(new HashMap<>());
if(input.getOutputObject() != null)
{
executeCodeInput.getContext().put("output", input.getOutputObject());
}
if(input.getScriptUtils() != null)
{
executeCodeInput.getContext().put("scriptUtils", input.getScriptUtils());
}
executeCodeInput.getContext().put("api", new ScriptApi());
executeCodeInput.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
/////////////////////////////////////////////////////////////////////////////////////////////////
// let caller supply a logger, or by default use StoreScriptLogAndScriptLogLineExecutionLogger //
/////////////////////////////////////////////////////////////////////////////////////////////////
QCodeExecutionLoggerInterface executionLogger = Objects.requireNonNullElseGet(input.getLogger(), () -> new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
executeCodeInput.setExecutionLogger(executionLogger);
if(executionLogger instanceof ScriptExecutionLoggerInterface scriptExecutionLoggerInterface)
{
////////////////////////////////////////////////////////////////////////////////////////////////////
// if logger is aware of scripts (as opposed to a generic CodeExecution logger), give it the ids. //
////////////////////////////////////////////////////////////////////////////////////////////////////
scriptExecutionLoggerInterface.setScriptId(scriptRevision.getScriptId());
scriptExecutionLoggerInterface.setScriptRevisionId(scriptRevision.getId());
}
ExecuteCodeOutput executeCodeOutput = new ExecuteCodeOutput();
new ExecuteCodeAction().run(executeCodeInput, executeCodeOutput);
output.setOutput(executeCodeOutput.getOutput());
output.setLogger(executionLogger);
}
catch(Exception e)
{
output.setException(Optional.of(e));
}
}
/*******************************************************************************
**
*******************************************************************************/
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("currentScriptRevision")));
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

@ -0,0 +1,236 @@
/*
* 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).
*******************************************************************************/
public class ScriptApi 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

@ -24,18 +24,35 @@ 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.Set;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.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.audits.AuditLevel;
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.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.ValueUtils;
/*******************************************************************************
@ -46,6 +63,8 @@ public class DeleteAction
{
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to delete";
/*******************************************************************************
@ -57,7 +76,6 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
// todo pre-customization - just get to modify the request?
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
{
@ -81,10 +99,186 @@ public class DeleteAction
}
}
DeleteOutput deleteResult = deleteInterface.execute(deleteInput);
// todo post-customization - can do whatever w/ the result if you want
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
List<QRecord> recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit);
return deleteResult;
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// merge the backend's output with any validation errors we found (whose ids wouldn't have gotten into the backend delete) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
if(outputRecordsWithErrors == null)
{
deleteOutput.setRecordsWithErrors(new ArrayList<>());
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
}
outputRecordsWithErrors.addAll(recordsWithValidationErrors);
manageAssociations(deleteInput);
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
return deleteOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(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, deleteInput.getPrimaryKeys()));
}
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();
DeleteInput nextLevelDeleteInput = new DeleteInput();
nextLevelDeleteInput.setTransaction(deleteInput.getTransaction());
nextLevelDeleteInput.setTableName(association.getAssociatedTableName());
nextLevelDeleteInput.setPrimaryKeys(associatedKeys);
DeleteOutput nextLevelDeleteOutput = new DeleteAction().execute(nextLevelDeleteInput);
}
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> getRecordListForAuditIfNeeded(DeleteInput deleteInput) throws QException
{
List<QRecord> recordListForAudit = null;
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(deleteInput);
if(AuditLevel.RECORD.equals(auditLevel) || AuditLevel.FIELD.equals(auditLevel))
{
List<Serializable> primaryKeyList = deleteInput.getPrimaryKeys();
if(CollectionUtils.nullSafeIsEmpty(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
{
primaryKeyList = getPrimaryKeysFromQueryFilter(deleteInput);
}
if(CollectionUtils.nullSafeHasContents(primaryKeyList))
{
////////////////////////////////////////////////////////////////////////////////////
// always fetch the records - we'll use them anyway for checking not-exist below //
////////////////////////////////////////////////////////////////////////////////////
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);
recordListForAudit = queryOutput.getRecords();
}
}
return (recordListForAudit);
}
/*******************************************************************************
** 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!
**
** This method, if it finds any missing records, will:
** - remove those ids from the deleteInput
** - create a QRecord with that id and a not-found error message.
*******************************************************************************/
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());
Set<Serializable> primaryKeysToRemoveFromInput = new HashSet<>();
List<List<Serializable>> pages = CollectionUtils.getPages(deleteInput.getPrimaryKeys(), 1000);
for(List<Serializable> page : pages)
{
List<Serializable> primaryKeysToLookup = new ArrayList<>();
for(Serializable primaryKeyValue : page)
{
if(primaryKeyValue != null)
{
primaryKeysToLookup.add(primaryKeyValue);
}
}
Map<Serializable, QRecord> lookedUpRecords = new HashMap<>();
if(CollectionUtils.nullSafeHasContents(oldRecordList))
{
for(QRecord record : oldRecordList)
{
Serializable primaryKeyValue = record.getValue(table.getPrimaryKeyField());
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
lookedUpRecords.put(primaryKeyValue, record);
}
}
else if(!primaryKeysToLookup.isEmpty())
{
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(table.getName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeysToLookup)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
for(Serializable primaryKeyValue : page)
{
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
if(!lookedUpRecords.containsKey(primaryKeyValue))
{
QRecord recordWithError = new QRecord();
recordsWithErrors.add(recordWithError);
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
recordWithError.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + primaryKeyValue);
primaryKeysToRemoveFromInput.add(primaryKeyValue);
}
}
/////////////////////////////////////////////////////////////////
// do one mass removal of any bad keys from the input key list //
/////////////////////////////////////////////////////////////////
if(!primaryKeysToRemoveFromInput.isEmpty())
{
deleteInput.getPrimaryKeys().removeAll(primaryKeysToRemoveFromInput);
primaryKeysToRemoveFromInput.clear();
}
}
return (recordsWithErrors);
}
@ -102,6 +296,7 @@ public class DeleteAction
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(deleteInput.getTableName());
queryInput.setFilter(deleteInput.getQueryFilter());
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);

View File

@ -36,6 +36,8 @@ import com.kingsrook.qqq.backend.core.actions.interfaces.GetInterface;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
@ -53,6 +55,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang.NotImplementedException;
@ -64,6 +67,8 @@ import org.apache.commons.lang.NotImplementedException;
*******************************************************************************/
public class GetAction
{
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
private Optional<AbstractPostQueryCustomizer> postGetRecordCustomizer;
private GetInput getInput;
@ -125,13 +130,34 @@ public class GetAction
QRecord recordFromSource = tryToGetFromCacheSource(getInput, getOutput);
if(recordFromSource != null)
{
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
boolean shouldCacheRecord = true;
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
getOutput.setRecord(insertOutput.getRecords().get(0));
////////////////////////////////////////////////////////////////////////////////
// see if there are any exclustions that need to be considered for this table //
////////////////////////////////////////////////////////////////////////////////
recordMatchExclusionLoop:
for(CacheUseCase useCase : CollectionUtils.nonNullList(table.getCacheOf().getUseCases()))
{
for(QQueryFilter filter : CollectionUtils.nonNullList(useCase.getExcludeRecordsMatching()))
{
if(BackendQueryFilterUtils.doesRecordMatch(filter, recordToCache))
{
LOG.info("Not caching record because it matches a use case's filter exclusion", new LogPair("record", recordToCache), new LogPair("filter", filter));
shouldCacheRecord = false;
break recordMatchExclusionLoop;
}
}
}
if(shouldCacheRecord)
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getInput.getTableName());
insertInput.setRecords(List.of(recordToCache));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
getOutput.setRecord(insertOutput.getRecords().get(0));
}
}
}
else
@ -257,7 +283,9 @@ public class GetAction
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
return (sourceGetOutput.getRecord());
QRecord outputRecord = sourceGetOutput.getRecord();
return (outputRecord);
}
@ -301,6 +329,8 @@ public class GetAction
}
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
QueryOutput queryOutput = new QueryAction().execute(queryInput);

View File

@ -23,32 +23,43 @@ 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.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.UniqueKeyHelper;
import com.kingsrook.qqq.backend.core.actions.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.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.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -70,20 +81,42 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
ActionHelper.validateSession(insertInput);
QTableMetaData table = insertInput.getTable();
if(table == null)
{
throw (new QException("Error: Undefined table: " + insertInput.getTableName()));
}
Optional<AbstractPostInsertCustomizer> postInsertCustomizer = QCodeLoader.getTableCustomizer(AbstractPostInsertCustomizer.class, table, TableCustomizers.POST_INSERT_RECORD.getRole());
setAutomationStatusField(insertInput);
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
// todo - need to handle records with errors coming out of here...
QBackendModuleInterface qModule = getBackendModuleInterface(insertInput);
// todo pre-customization - just get to modify the request?
ValueBehaviorApplier.applyFieldBehaviors(insertInput.getInstance(), table, insertInput.getRecords());
setErrorsIfUniqueKeyErrors(insertInput, table);
validateRequiredFields(insertInput);
ValidateRecordSecurityLockHelper.validateSecurityFields(insertInput.getTable(), insertInput.getRecords(), ValidateRecordSecurityLockHelper.Action.INSERT);
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
if(CollectionUtils.nullSafeHasContents(errors))
{
LOG.warn("Errors in insertAction", logPair("tableName", table.getName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
}
manageAssociations(table, insertOutput.getRecords(), insertInput.getTransaction());
// todo post-customization - can do whatever w/ the result if you want
if(insertInput.getOmitDmlAudit())
{
LOG.debug("Requested to omit DML audit");
}
else
{
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(insertInput).withRecordList(insertOutput.getRecords()));
}
if(postInsertCustomizer.isPresent())
{
postInsertCustomizer.get().setInsertInput(insertInput);
@ -95,6 +128,70 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
/*******************************************************************************
**
*******************************************************************************/
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("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(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
for(QRecord associatedRecord : CollectionUtils.nonNullList(record.getAssociatedRecords().get(association.getName())))
{
for(JoinOn joinOn : join.getJoinOns())
{
associatedRecord.setValue(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
nextLevelInserts.add(associatedRecord);
}
}
}
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(transaction);
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -116,7 +213,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
List<UniqueKey> uniqueKeys = CollectionUtils.nonNullList(table.getUniqueKeys());
for(UniqueKey uniqueKey : uniqueKeys)
{
existingKeys.put(uniqueKey, UniqueKeyHelper.getExistingKeys(insertInput, insertInput.getTransaction(), table, insertInput.getRecords(), uniqueKey));
existingKeys.put(uniqueKey, UniqueKeyHelper.getExistingKeys(insertInput.getTransaction(), table, insertInput.getRecords(), uniqueKey).keySet());
}
/////////////////////////////////////

View File

@ -22,8 +22,13 @@
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.Optional;
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;
@ -31,13 +36,23 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.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.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.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;
/*******************************************************************************
@ -70,6 +85,14 @@ public class QueryAction
queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions);
}
if(queryInput.getIncludeAssociations() && queryInput.getRecordPipe() != null)
{
//////////////////////////////////////////////
// todo - support this in the future maybe? //
//////////////////////////////////////////////
throw (new QException("Associations may not be fetched into a RecordPipe."));
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request?
@ -86,11 +109,119 @@ public class QueryAction
postRecordActions(queryOutput.getRecords());
}
if(queryInput.getIncludeAssociations())
{
manageAssociations(queryInput, queryOutput);
}
return queryOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QueryInput queryInput, QueryOutput queryOutput) 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 : queryOutput.getRecords())
{
Serializable value = record.getValue(joinOn.getLeftField());
values.add(value);
outerResultMap.add(List.of(value), record);
}
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.IN, new ArrayList<>(values)));
}
else
{
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QRecord record : queryOutput.getRecords())
{
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,

View File

@ -22,15 +22,46 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
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.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
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.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.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -39,6 +70,12 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
*******************************************************************************/
public class UpdateAction
{
private static final QLogger LOG = QLogger.getLogger(UpdateAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to update";
/*******************************************************************************
**
*******************************************************************************/
@ -50,12 +87,310 @@ public class UpdateAction
ValueBehaviorApplier.applyFieldBehaviors(updateInput.getInstance(), updateInput.getTable(), updateInput.getRecords());
// todo - need to handle records with errors coming out of here...
List<QRecord> oldRecordList = getOldRecordListForAuditIfNeeded(updateInput);
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend());
validatePrimaryKeysAreGiven(updateInput);
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList);
validateRequiredFields(updateInput);
ValidateRecordSecurityLockHelper.validateSecurityFields(updateInput.getTable(), updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
// todo pre-customization - just get to modify the request?
UpdateOutput updateResult = qModule.getUpdateInterface().execute(updateInput);
UpdateOutput updateOutput = qModule.getUpdateInterface().execute(updateInput);
// todo post-customization - can do whatever w/ the result if you want
return updateResult;
List<String> errors = updateOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
if(CollectionUtils.nullSafeHasContents(errors))
{
LOG.warn("Errors in updateAction", logPair("tableName", updateInput.getTableName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
}
manageAssociations(updateInput);
if(updateInput.getOmitDmlAudit())
{
LOG.debug("Requested to omit DML audit");
}
else
{
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(updateInput).withRecordList(updateOutput.getRecords()).withOldRecordList(oldRecordList));
}
return updateOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private void validatePrimaryKeysAreGiven(UpdateInput updateInput)
{
QTableMetaData table = updateInput.getTable();
for(QRecord record : CollectionUtils.nonNullList(updateInput.getRecords()))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to update a record, we must have its primary key value - so - check - if it's missing, mark it as an error //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(record.getValue(table.getPrimaryKeyField()) == null)
{
record.addError("Missing value in primary key field");
}
}
}
/*******************************************************************************
** 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!
*******************************************************************************/
private void validateRecordsExistAndCanBeAccessed(UpdateInput updateInput, List<QRecord> oldRecordList) throws QException
{
QTableMetaData table = updateInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
{
List<Serializable> primaryKeysToLookup = new ArrayList<>();
for(QRecord record : page)
{
Serializable primaryKeyValue = record.getValue(table.getPrimaryKeyField());
if(primaryKeyValue != null)
{
primaryKeysToLookup.add(primaryKeyValue);
}
}
Map<Serializable, QRecord> lookedUpRecords = new HashMap<>();
if(CollectionUtils.nullSafeHasContents(oldRecordList))
{
for(QRecord record : oldRecordList)
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
else if(!primaryKeysToLookup.isEmpty())
{
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(updateInput.getTransaction());
queryInput.setTableName(table.getName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeysToLookup)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
for(QRecord record : page)
{
Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField()));
if(value == null)
{
continue;
}
if(!lookedUpRecords.containsKey(value))
{
record.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + value);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateRequiredFields(UpdateInput updateInput)
{
QTableMetaData table = updateInput.getTable();
Set<QFieldMetaData> requiredFields = table.getFields().values().stream()
.filter(f -> f.getIsRequired())
.collect(Collectors.toSet());
if(!requiredFields.isEmpty())
{
for(QRecord record : CollectionUtils.nonNullList(updateInput.getRecords()))
{
for(QFieldMetaData requiredField : requiredFields)
{
/////////////////////////////////////////////////////////////////////////////////////////////
// only consider fields that were set in the record to be updated (e.g., "patch" semantic) //
/////////////////////////////////////////////////////////////////////////////////////////////
if(record.getValues().containsKey(requiredField.getName()))
{
if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals("")))
{
record.addError("Missing value in required field: " + requiredField.getLabel());
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(UpdateInput updateInput) throws QException
{
QTableMetaData table = updateInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
// e.g., order -> orderLine
QTableMetaData associatedTable = QContext.getQInstance().getTable(association.getAssociatedTableName());
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 500))
{
List<QRecord> nextLevelUpdates = new ArrayList<>();
List<QRecord> nextLevelInserts = new ArrayList<>();
QQueryFilter findDeletesFilter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
boolean lookForDeletes = false;
//////////////////////////////////////////////////////
// for each updated record, look at as associations //
//////////////////////////////////////////////////////
for(QRecord record : page)
{
if(record.getAssociatedRecords() != null && record.getAssociatedRecords().containsKey(association.getName()))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// build a sub-query to find the children of this record - and we'll exclude (below) any whose ids are given //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
QQueryFilter subFilter = new QQueryFilter();
findDeletesFilter.addSubFilter(subFilter);
lookForDeletes = true;
List<Serializable> idsBeingUpdated = new ArrayList<>();
for(JoinOn joinOn : join.getJoinOns())
{
subFilter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, record.getValue(joinOn.getLeftField())));
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for any associated records present here, figure out if they're being inserted (no primaryKey) or updated //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(QRecord associatedRecord : CollectionUtils.nonNullList(record.getAssociatedRecords().get(association.getName())))
{
Serializable associatedId = associatedRecord.getValue(associatedTable.getPrimaryKeyField());
if(associatedId == null)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// if inserting, add to the inserts list, and propagate values from the header record down to the child //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
for(JoinOn joinOn : join.getJoinOns())
{
associatedRecord.setValue(joinOn.getRightField(), record.getValue(joinOn.getLeftField()));
}
nextLevelInserts.add(associatedRecord);
}
else
{
///////////////////////////////////////////////////////////////////////////////
// if updating, add to the updates list, and add the id as one to not delete //
///////////////////////////////////////////////////////////////////////////////
idsBeingUpdated.add(associatedId);
nextLevelUpdates.add(associatedRecord);
}
}
if(!idsBeingUpdated.isEmpty())
{
///////////////////////////////////////////////////////////////////////////////
// if any records are being updated, add them to the query to NOT be deleted //
///////////////////////////////////////////////////////////////////////////////
subFilter.addCriteria(new QFilterCriteria(associatedTable.getPrimaryKeyField(), QCriteriaOperator.NOT_IN, idsBeingUpdated));
}
}
}
if(lookForDeletes)
{
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(updateInput.getTransaction());
queryInput.setTableName(associatedTable.getName());
queryInput.setFilter(findDeletesFilter);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
if(!queryOutput.getRecords().isEmpty())
{
LOG.debug("Deleting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", queryOutput.getRecords().size()));
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTransaction(updateInput.getTransaction());
deleteInput.setTableName(association.getAssociatedTableName());
deleteInput.setPrimaryKeys(queryOutput.getRecords().stream().map(r -> r.getValue(associatedTable.getPrimaryKeyField())).collect(Collectors.toList()));
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
}
}
if(CollectionUtils.nullSafeHasContents(nextLevelUpdates))
{
LOG.debug("Updating associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
UpdateInput nextLevelUpdateInput = new UpdateInput();
nextLevelUpdateInput.setTransaction(updateInput.getTransaction());
nextLevelUpdateInput.setTableName(association.getAssociatedTableName());
nextLevelUpdateInput.setRecords(nextLevelUpdates);
UpdateOutput nextLevelUpdateOutput = new UpdateAction().execute(nextLevelUpdateInput);
}
if(CollectionUtils.nullSafeHasContents(nextLevelInserts))
{
LOG.debug("Inserting associatedRecords", logPair("associatedTable", associatedTable.getName()), logPair("noOfRecords", nextLevelUpdates.size()));
InsertInput nextLevelInsertInput = new InsertInput();
nextLevelInsertInput.setTransaction(updateInput.getTransaction());
nextLevelInsertInput.setTableName(association.getAssociatedTableName());
nextLevelInsertInput.setRecords(nextLevelInserts);
InsertOutput nextLevelInsertOutput = new InsertAction().execute(nextLevelInsertInput);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static List<QRecord> getOldRecordListForAuditIfNeeded(UpdateInput updateInput)
{
if(updateInput.getOmitDmlAudit())
{
return (null);
}
try
{
AuditLevel auditLevel = DMLAuditAction.getAuditLevel(updateInput);
List<QRecord> oldRecordList = null;
if(AuditLevel.FIELD.equals(auditLevel))
{
String primaryKeyField = updateInput.getTable().getPrimaryKeyField();
List<Serializable> pkeysBeingUpdated = CollectionUtils.nonNullList(updateInput.getRecords()).stream().map(r -> r.getValue(primaryKeyField)).toList();
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(updateInput.getTransaction());
queryInput.setTableName(updateInput.getTableName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, pkeysBeingUpdated)));
// todo - need a limit? what if too many??
QueryOutput queryOutput = new QueryAction().execute(queryInput);
oldRecordList = queryOutput.getRecords();
}
return oldRecordList;
}
catch(Exception e)
{
LOG.warn("Error getting old record list for audit", e, logPair("table", updateInput.getTableName()));
return (null);
}
}

View File

@ -24,15 +24,14 @@ package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
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.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -55,10 +54,10 @@ public class UniqueKeyHelper
/*******************************************************************************
**
*******************************************************************************/
public static Set<List<Serializable>> getExistingKeys(AbstractActionInput actionInput, QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
{
List<String> ukFieldNames = uniqueKey.getFieldNames();
Set<List<Serializable>> existingRecords = new HashSet<>();
List<String> ukFieldNames = uniqueKey.getFieldNames();
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
if(ukFieldNames != null)
{
QueryInput queryInput = new QueryInput();
@ -114,8 +113,10 @@ public class UniqueKeyHelper
for(QRecord record : queryOutput.getRecords())
{
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record);
keyValues.ifPresent(existingRecords::add);
if(keyValues.isPresent())
{
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
}
}
}

View File

@ -0,0 +1,313 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.tables.helpers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class ValidateRecordSecurityLockHelper
{
private static final QLogger LOG = QLogger.getLogger(ValidateRecordSecurityLockHelper.class);
/*******************************************************************************
**
*******************************************************************************/
public enum Action
{
INSERT,
UPDATE,
SELECT
}
/*******************************************************************************
**
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
{
return;
}
////////////////////////////////
// actually check lock values //
////////////////////////////////
for(RecordSecurityLock recordSecurityLock : locksToCheck)
{
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()))
{
//////////////////////////////////////////////////////////////////////////////////
// handle the value being in the table we're inserting/updating (e.g., no join) //
//////////////////////////////////////////////////////////////////////////////////
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
for(QRecord record : records)
{
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()))
{
/////////////////////////////////////////////////////////////////////////
// if not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////
continue;
}
Serializable recordSecurityValue = record.getValue(field.getName());
validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action);
}
}
else
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(records, 500))
{
////////////////////////////////////////////////////////////////////////////////////////////////
// set up a query for joined records //
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
////////////////////////////////////////////////////////////////////////////////////////////////
QueryInput queryInput = new QueryInput();
queryInput.setTableName(leftMostJoin.getLeftTable());
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
queryInput.setFilter(filter);
for(String joinName : recordSecurityLock.getJoinNameChain())
{
///////////////////////////////////////
// we don't need the right-most join //
///////////////////////////////////////
if(!joinName.equals(rightMostJoin.getName()))
{
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
// also build up the query's sub-filters here (only adding them if they're unique). //
// e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice //
///////////////////////////////////////////////////////////////////////////////////////////////////
ListingHash<List<Serializable>, QRecord> inputRecordMapByJoinFields = new ListingHash<>();
for(QRecord inputRecord : inputRecordPage)
{
List<Serializable> inputRecordJoinValues = new ArrayList<>();
QQueryFilter subFilter = new QQueryFilter();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField());
inputRecordJoinValues.add(inputRecordValue);
subFilter.addCriteria(inputRecordValue == null
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
}
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
////////////////////////////////////////////////////////////////////////////////
// only add this sub-filter if it's for a list of keys we haven't seen before //
////////////////////////////////////////////////////////////////////////////////
filter.addSubFilter(subFilter);
}
inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// execute the query for joined records - then put them in a map with keys corresponding to the join values //
// e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QueryOutput queryOutput = new QueryAction().execute(queryInput);
Map<List<Serializable>, QRecord> joinRecordMapByJoinFields = new HashMap<>();
for(QRecord joinRecord : queryOutput.getRecords())
{
List<Serializable> joinRecordValues = new ArrayList<>();
for(JoinOn joinOn : rightMostJoin.getJoinOns())
{
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
{
joinValue = joinRecord.getValue(joinOn.getLeftField());
}
joinRecordValues.add(joinValue);
}
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// now for each input record, look for its joinRecord - if it isn't found, then this insert //
// isn't allowed. if it is found, then validate its value matches this session's security keys //
//////////////////////////////////////////////////////////////////////////////////////////////////
for(Map.Entry<List<Serializable>, List<QRecord>> entry : inputRecordMapByJoinFields.entrySet())
{
List<Serializable> inputRecordJoinValues = entry.getKey();
List<QRecord> inputRecords = entry.getValue();
if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues))
{
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
{
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
}
for(QRecord inputRecord : inputRecords)
{
validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action);
}
}
else
{
for(QRecord inputRecord : inputRecords)
{
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
{
inputRecord.addError("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
}
}
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table)
{
List<RecordSecurityLock> recordSecurityLocks = table.getRecordSecurityLocks();
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
////////////////////////////////////////
// if there are no locks, just return //
////////////////////////////////////////
if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks))
{
return (null);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
{
QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
LOG.debug("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock.");
}
else
{
locksToCheck.add(recordSecurityLock);
}
}
return (locksToCheck);
}
/*******************************************************************************
**
*******************************************************************************/
static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{
if(recordSecurityValue == null)
{
/////////////////////////////////////////////////////////////////
// handle null values - error if the NullValueBehavior is DENY //
/////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
{
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
record.addError("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel);
}
}
else
{
if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType))
{
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()))
{
///////////////////////////////////////////////////////////////////////////////////////////////
// avoid telling the user a value from a foreign record that they didn't pass in themselves. //
///////////////////////////////////////////////////////////////////////////////////////////////
record.addError("You do not have permission to " + action.name().toLowerCase() + " this record.");
}
else
{
QFieldMetaData field = table.getField(recordSecurityLock.getFieldName());
record.addError("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel());
}
}
}
}
}

View File

@ -23,21 +23,81 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
** Interface to be implemented by user-defined code that serves as the backing
** for a CUSTOM type possibleValueSource
**
** Type parameter `T` is the id-type of the possible value.
*******************************************************************************/
public interface QCustomPossibleValueProvider
public interface QCustomPossibleValueProvider<T extends Serializable>
{
/*******************************************************************************
**
*******************************************************************************/
QPossibleValue<T> getPossibleValue(Serializable idValue);
/*******************************************************************************
**
*******************************************************************************/
QPossibleValue<?> getPossibleValue(Serializable idValue);
List<QPossibleValue<T>> search(SearchPossibleValueSourceInput input) throws QException;
// todo - get/search list of possible values
/*******************************************************************************
** The input list of ids might come through as a type that isn't the same as
** the type of the ids in the enum (e.g., strings from a frontend, integers
** in an enum). So, this method looks maps a list of input ids to the requested type.
*******************************************************************************/
default List<T> convertInputIdsToIdType(Class<T> type, List<Serializable> inputIdList)
{
List<T> rs = new ArrayList<>();
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
{
return (rs);
}
for(Serializable serializable : inputIdList)
{
rs.add(ValueUtils.getValueAsType(type, serializable));
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
default boolean doesPossibleValueMatchSearchInput(List<T> idsInType, QPossibleValue<T> possibleValue, SearchPossibleValueSourceInput input)
{
boolean match = false;
if(input.getIdList() != null)
{
if(idsInType.contains(possibleValue.getId()))
{
match = true;
}
}
else
{
if(StringUtils.hasContent(input.getSearchTerm()))
{
match = possibleValue.getLabel().toLowerCase().startsWith(input.getSearchTerm().toLowerCase());
}
else
{
match = true;
}
}
return match;
}
}

View File

@ -34,6 +34,7 @@ import java.util.Objects;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
@ -56,6 +57,7 @@ import com.kingsrook.qqq.backend.core.utils.ListingHash;
import com.kingsrook.qqq.backend.core.utils.Pair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -66,28 +68,31 @@ public class QPossibleValueTranslator
{
private static final QLogger LOG = QLogger.getLogger(QPossibleValueTranslator.class);
private final QInstance qInstance;
private final QSession session;
///////////////////////////////////////////////////////
// top-level keys are pvsNames (not table names) //
// 2nd-level keys are pkey values from the PVS table //
///////////////////////////////////////////////////////
private Map<String, Map<Serializable, String>> possibleValueCache;
private Map<String, Map<Serializable, String>> possibleValueCache = new HashMap<>();
// todo not commit - remove instance & session - use Context
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public QPossibleValueTranslator()
{
}
/*******************************************************************************
**
*******************************************************************************/
public QPossibleValueTranslator(QInstance qInstance, QSession session)
{
this.qInstance = qInstance;
this.session = session;
this.possibleValueCache = new HashMap<>();
}
@ -140,7 +145,7 @@ public class QPossibleValueTranslator
{
try
{
QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable());
QTableMetaData joinTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
for(QFieldMetaData field : joinTable.getFields().values())
{
String joinFieldName = Objects.requireNonNullElse(queryJoin.getAlias(), joinTable.getName()) + "." + field.getName();
@ -151,7 +156,7 @@ public class QPossibleValueTranslator
///////////////////////////////////////////////
// avoid circling-back upon the source table //
///////////////////////////////////////////////
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(field.getPossibleValueSourceName());
if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType()) && table.getName().equals(possibleValueSource.getTableName()))
{
continue;
@ -211,7 +216,7 @@ public class QPossibleValueTranslator
*******************************************************************************/
public String translatePossibleValue(QFieldMetaData field, Serializable value)
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource == null)
{
LOG.error("Missing possible value source named [" + field.getPossibleValueSourceName() + "] when formatting value for field [" + field.getName() + "]");
@ -413,7 +418,16 @@ public class QPossibleValueTranslator
if(queryJoin.getSelect())
{
String aliasOrTableName = Objects.requireNonNullElse(queryJoin.getAlias(), queryJoin.getJoinTable());
primePvsCacheTableListingHashLoader(qInstance.getTable(queryJoin.getJoinTable()), fieldsByPvsTable, pvsesByTable, aliasOrTableName + ".", queryJoin.getJoinTable(), limitedToFieldNames);
primePvsCacheTableListingHashLoader(QContext.getQInstance().getTable(queryJoin.getJoinTable()), fieldsByPvsTable, pvsesByTable, aliasOrTableName + ".", queryJoin.getJoinTable(), limitedToFieldNames);
}
}
for(Map.Entry<String, Map<Serializable, String>> entry : possibleValueCache.entrySet())
{
int size = entry.getValue().size();
if(size > 50_000)
{
LOG.debug("Found a big PVS cache - clearing it.", logPair("name", entry.getKey()), logPair("size", size));
}
}
@ -464,7 +478,7 @@ public class QPossibleValueTranslator
{
for(QFieldMetaData field : table.getFields().values())
{
QPossibleValueSource possibleValueSource = qInstance.getPossibleValueSource(field.getPossibleValueSourceName());
QPossibleValueSource possibleValueSource = QContext.getQInstance().getPossibleValueSource(field.getPossibleValueSourceName());
if(possibleValueSource != null && possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
if(limitedToFieldNames != null && !limitedToFieldNames.contains(fieldNamePrefix + field.getName()))
@ -500,7 +514,7 @@ public class QPossibleValueTranslator
try
{
String primaryKeyField = qInstance.getTable(tableName).getPrimaryKeyField();
String primaryKeyField = QContext.getQInstance().getTable(tableName).getPrimaryKeyField();
for(List<Serializable> page : CollectionUtils.getPages(values, 1000))
{
@ -521,7 +535,7 @@ public class QPossibleValueTranslator
{
if(possibleValueSource.getType().equals(QPossibleValueSourceType.TABLE))
{
QTableMetaData table = qInstance.getTable(possibleValueSource.getTableName());
QTableMetaData table = QContext.getQInstance().getTable(possibleValueSource.getTableName());
for(String recordLabelField : CollectionUtils.nonNullList(table.getRecordLabelFields()))
{
QFieldMetaData field = table.getField(recordLabelField);

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.actions.values;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.List;
@ -32,7 +34,6 @@ import java.util.Map;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -46,8 +47,10 @@ public class QValueFormatter
{
private static final QLogger LOG = QLogger.getLogger(QValueFormatter.class);
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd h:mm a");
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a");
private static DateTimeFormatter dateTimeWithZoneFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a z");
private static DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static DateTimeFormatter localTimeFormatter = DateTimeFormatter.ofPattern("h:mm:ss a");
@ -56,23 +59,6 @@ public class QValueFormatter
*******************************************************************************/
public static String formatValue(QFieldMetaData field, Serializable value)
{
if(QFieldType.BOOLEAN.equals(field.getType()))
{
Boolean b = ValueUtils.getValueAsBoolean(value);
if(b == null)
{
return (null);
}
else if(b)
{
return ("Yes");
}
else
{
return ("No");
}
}
return (formatValue(field.getDisplayFormat(), field.getName(), value));
}
@ -102,6 +88,22 @@ public class QValueFormatter
return (null);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// try to apply some type-specific defaults, if we were requested to just format as a string. //
////////////////////////////////////////////////////////////////////////////////////////////////
if("%s".equals(displayFormat))
{
if(value instanceof Boolean b)
{
return formatBoolean(b);
}
if(value instanceof LocalTime lt)
{
return formatLocalTime(lt);
}
}
////////////////////////////////////////////////////////
// if the field has a display format, try to apply it //
////////////////////////////////////////////////////////
@ -168,6 +170,26 @@ public class QValueFormatter
/*******************************************************************************
**
*******************************************************************************/
public static String formatDateTimeWithZone(ZonedDateTime dateTime)
{
return (dateTimeWithZoneFormatter.format(dateTime));
}
/*******************************************************************************
**
*******************************************************************************/
public static String formatLocalTime(LocalTime localTime)
{
return (localTimeFormatter.format(localTime));
}
/*******************************************************************************
** Make a string from a table's recordLabelFormat and fields, for a given record.
*******************************************************************************/
@ -260,7 +282,8 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values
** For a list of records, set their recordLabels and display values - including
** record label (e.g., from the table meta data).
*******************************************************************************/
public static void setDisplayValuesInRecords(QTableMetaData table, List<QRecord> records)
{
@ -278,6 +301,24 @@ public class QValueFormatter
/*******************************************************************************
** For a list of records, set their recordLabels and display values
*******************************************************************************/
public static void setDisplayValuesInRecords(Collection<QFieldMetaData> fields, List<QRecord> records)
{
if(records == null)
{
return;
}
for(QRecord record : records)
{
setDisplayValuesInRecord(fields, record);
}
}
/*******************************************************************************
** For a list of records, set their display values
*******************************************************************************/
@ -293,4 +334,25 @@ public class QValueFormatter
}
}
/*******************************************************************************
**
*******************************************************************************/
public static String formatBoolean(Boolean b)
{
if(b == null)
{
return (null);
}
else if(b)
{
return ("Yes");
}
else
{
return ("No");
}
}
}

View File

@ -26,6 +26,7 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
@ -275,8 +276,12 @@ public class SearchPossibleValueSourceAction
{
try
{
// QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
// return (formatPossibleValue(possibleValueSource, customPossibleValueProvider.getPossibleValue(value)));
QCustomPossibleValueProvider customPossibleValueProvider = QCodeLoader.getCustomPossibleValueProvider(possibleValueSource);
List<QPossibleValue<?>> possibleValues = customPossibleValueProvider.search(input);
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
output.setResults(possibleValues);
return (output);
}
catch(Exception e)
{

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.context;
import java.util.Optional;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
@ -243,4 +244,19 @@ public class QContext
{
qBackendTransactionThreadLocal.remove();
}
/*******************************************************************************
**
*******************************************************************************/
public static Optional<AbstractActionInput> getFirstActionInStack()
{
if(actionStackThreadLocal.get() == null || actionStackThreadLocal.get().isEmpty())
{
return (Optional.empty());
}
return (Optional.of(actionStackThreadLocal.get().get(0)));
}
}

View File

@ -0,0 +1,113 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.exceptions;
/*******************************************************************************
* Exception thrown doing authentication
*
*******************************************************************************/
public class AccessTokenException extends QAuthenticationException
{
private Integer statusCode;
/*******************************************************************************
** Constructor of message
**
*******************************************************************************/
public AccessTokenException(String message)
{
super(message);
}
/*******************************************************************************
** Constructor of message
**
*******************************************************************************/
public AccessTokenException(String message, int statusCode)
{
super(message);
this.statusCode = statusCode;
}
/*******************************************************************************
** Constructor of message & cause
**
*******************************************************************************/
public AccessTokenException(String message, Throwable cause)
{
super(message, cause);
}
/*******************************************************************************
** Constructor of message & cause
**
*******************************************************************************/
public AccessTokenException(String message, Throwable cause, int statusCode)
{
super(message, cause);
this.statusCode = statusCode;
}
/*******************************************************************************
** Getter for statusCode
**
*******************************************************************************/
public Integer getStatusCode()
{
return statusCode;
}
/*******************************************************************************
** Setter for statusCode
**
*******************************************************************************/
public void setStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
}
/*******************************************************************************
** Fluent setter for statusCode
**
*******************************************************************************/
public AccessTokenException withStatusCode(Integer statusCode)
{
this.statusCode = statusCode;
return (this);
}
}

View File

@ -60,6 +60,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QMiddlewareTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.processes.implementations.bulk.delete.BulkDeleteTransformStep;
@ -180,6 +181,11 @@ public class QInstanceEnricher
if(table.getFields() != null)
{
table.getFields().values().forEach(this::enrichField);
for(QMiddlewareTableMetaData middlewareTableMetaData : CollectionUtils.nonNullMap(table.getMiddlewareMetaData()).values())
{
middlewareTableMetaData.enrich(table);
}
}
if(CollectionUtils.nullSafeIsEmpty(table.getSections()))
@ -197,6 +203,20 @@ public class QInstanceEnricher
}
enrichPermissionRules(table);
enrichAuditRules(table);
}
/*******************************************************************************
**
*******************************************************************************/
private void enrichAuditRules(QTableMetaData table)
{
if(table.getAuditRules() == null && qInstance.getDefaultAuditRules() != null)
{
table.setAuditRules(qInstance.getDefaultAuditRules());
}
}
@ -312,14 +332,7 @@ public class QInstanceEnricher
{
if(!StringUtils.hasContent(field.getLabel()))
{
if(configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels && StringUtils.hasContent(field.getPossibleValueSourceName()) && field.getName() != null && field.getName().endsWith("Id"))
{
field.setLabel(nameToLabel(field.getName().substring(0, field.getName().length() - 2)));
}
else
{
field.setLabel(nameToLabel(field.getName()));
}
fieldNameToLabel(field);
}
//////////////////////////////////////////////////////////////////////////
@ -327,7 +340,7 @@ public class QInstanceEnricher
// and that PVS exists in the instance //
// and it's a table-type PVS and the table name is set //
// and it's a valid table in the instance, and the table is in some app //
// and the field doesn't have a LINK adornment //
// and the field doesn't (already) have a LINK or CHIP adornment //
// then add a link-to-record-from-table adornment to the field. //
//////////////////////////////////////////////////////////////////////////
if(StringUtils.hasContent(field.getPossibleValueSourceName()))
@ -340,7 +353,15 @@ public class QInstanceEnricher
{
if(qInstance.getTable(tableName) != null && doesAnyAppHaveTable(tableName))
{
if(field.getAdornments() == null || field.getAdornments().stream().noneMatch(a -> AdornmentType.LINK.equals(a.getType())))
boolean hasLinkAdornment = false;
boolean hasChipAdornment = false;
if(field.getAdornments() != null)
{
hasLinkAdornment = field.getAdornments().stream().anyMatch(a -> AdornmentType.LINK.equals(a.getType()));
hasChipAdornment = field.getAdornments().stream().anyMatch(a -> AdornmentType.CHIP.equals(a.getType()));
}
if(!hasLinkAdornment && !hasChipAdornment)
{
field.withFieldAdornment(new FieldAdornment().withType(AdornmentType.LINK)
.withValue(AdornmentType.LinkValues.TO_RECORD_FROM_TABLE, tableName));
@ -353,6 +374,23 @@ public class QInstanceEnricher
/*******************************************************************************
**
*******************************************************************************/
public void fieldNameToLabel(QFieldMetaData field)
{
if(configRemoveIdFromNameWhenCreatingPossibleValueFieldLabels && StringUtils.hasContent(field.getPossibleValueSourceName()) && field.getName() != null && field.getName().endsWith("Id"))
{
field.setLabel(nameToLabel(field.getName().substring(0, field.getName().length() - 2)));
}
else
{
field.setLabel(nameToLabel(field.getName()));
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.instances;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@ -46,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QMiddlewareInstanceMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
@ -66,6 +68,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
@ -141,6 +144,7 @@ public class QInstanceValidator
validateQueuesAndProviders(qInstance);
validateJoins(qInstance);
validateSecurityKeyTypes(qInstance);
validateMiddlewareMetaData(qInstance);
validateUniqueTopLevelNames(qInstance);
}
@ -159,6 +163,19 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateMiddlewareMetaData(QInstance qInstance)
{
for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values())
{
middlewareInstanceMetaData.validate(qInstance, this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -441,12 +458,38 @@ public class QInstanceValidator
validateAssociatedScripts(table);
validateTableCacheOf(qInstance, table);
validateTableRecordSecurityLocks(qInstance, table);
validateTableAssociations(qInstance, table);
});
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateTableAssociations(QInstance qInstance, QTableMetaData table)
{
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
if(assertCondition(StringUtils.hasContent(association.getName()), "missing a name for an Association on table " + table.getName()))
{
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
if(assertCondition(StringUtils.hasContent(association.getAssociatedTableName()), "missing associatedTableName" + messageSuffix))
{
assertCondition(qInstance.getTable(association.getAssociatedTableName()) != null, "unrecognized associatedTableName " + association.getAssociatedTableName() + messageSuffix);
}
if(assertCondition(StringUtils.hasContent(association.getJoinName()), "missing joinName" + messageSuffix))
{
assertCondition(qInstance.getJoin(association.getJoinName()) != null, "unrecognized joinName " + association.getJoinName() + messageSuffix);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -454,6 +497,7 @@ public class QInstanceValidator
{
String prefix = "Table " + table.getName() + " ";
RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
@ -480,21 +524,61 @@ public class QInstanceValidator
////////////////////////////////////////////////////////////////////////////////
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
{
List<QueryJoin> joins = new ArrayList<>();
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
if(fieldName.contains("."))
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join.getLeftTable().equals(table.getName()))
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
joins.add(new QueryJoin(join));
}
else if(join.getRightTable().equals(table.getName()))
{
joins.add(new QueryJoin(join.flip()));
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
}
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
else
{
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
{
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
@ -887,7 +971,8 @@ public class QInstanceValidator
//////////////////////////////////////////
// otherwise, just append the exception //
//////////////////////////////////////////
errors.add(prefix + ": " + e);
e.printStackTrace();
errors.add(prefix + ": " + e.getMessage());
}
}
}

View File

@ -187,6 +187,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void trace(String message, Throwable t, LogPair... logPairs)
{
logger.trace(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
@ -237,6 +247,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void debug(String message, Throwable t, LogPair... logPairs)
{
logger.debug(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
@ -307,6 +327,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void info(String message, Throwable t, LogPair... logPairs)
{
logger.info(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
@ -357,6 +387,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void warn(String message, Throwable t, LogPair... logPairs)
{
logger.warn(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
@ -407,6 +447,16 @@ public class QLogger
/*******************************************************************************
**
*******************************************************************************/
public void error(String message, Throwable t, LogPair... logPairs)
{
logger.error(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.context.QContext;
@ -84,6 +85,7 @@ public class AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public QAuthenticationMetaData getAuthenticationMetaData()
{
return (getInstance().getAuthentication());
@ -95,6 +97,7 @@ public class AbstractActionInput
** Getter for instance
**
*******************************************************************************/
@JsonIgnore
public QInstance getInstance()
{
return (QContext.getQInstance());
@ -106,6 +109,7 @@ public class AbstractActionInput
** Getter for session
**
*******************************************************************************/
@JsonIgnore
public QSession getSession()
{
return (QContext.getQSession());
@ -117,6 +121,7 @@ public class AbstractActionInput
** Getter for asyncJobCallback
**
*******************************************************************************/
@JsonIgnore
public AsyncJobCallback getAsyncJobCallback()
{
if(asyncJobCallback == null)

View File

@ -23,208 +23,72 @@ package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
/*******************************************************************************
**
** Input object for the audit action - an object which contains a list of "single"
** audit inputs - e.g., the data needed to insert 1 audit.
*******************************************************************************/
public class AuditInput extends AbstractActionInput
public class AuditInput extends AbstractActionInput implements Serializable
{
private String auditTableName;
private String auditUserName;
private Instant timestamp;
private String message;
private List<Integer> recordIdList;
private Map<String, Serializable> securityKeyValues;
private List<AuditSingleInput> auditSingleInputList = new ArrayList<>();
/*******************************************************************************
** Getter for auditTableName
** Getter for auditSingleInputList
*******************************************************************************/
public String getAuditTableName()
public List<AuditSingleInput> getAuditSingleInputList()
{
return (this.auditTableName);
return (this.auditSingleInputList);
}
/*******************************************************************************
** Setter for auditTableName
** Setter for auditSingleInputList
*******************************************************************************/
public void setAuditTableName(String auditTableName)
public void setAuditSingleInputList(List<AuditSingleInput> auditSingleInputList)
{
this.auditTableName = auditTableName;
this.auditSingleInputList = auditSingleInputList;
}
/*******************************************************************************
** Fluent setter for auditTableName
** Fluent setter for auditSingleInputList
*******************************************************************************/
public AuditInput withAuditTableName(String auditTableName)
public AuditInput withAuditSingleInputList(List<AuditSingleInput> auditSingleInputList)
{
this.auditTableName = auditTableName;
this.auditSingleInputList = auditSingleInputList;
return (this);
}
/*******************************************************************************
** Getter for auditUserName
** Add a single auditSingleInput
*******************************************************************************/
public String getAuditUserName()
public void addAuditSingleInput(AuditSingleInput auditSingleInput)
{
return (this.auditUserName);
if(this.auditSingleInputList == null)
{
this.auditSingleInputList = new ArrayList<>();
}
this.auditSingleInputList.add(auditSingleInput);
}
/*******************************************************************************
** Setter for auditUserName
** Fluent setter to add a single auditSingleInput
*******************************************************************************/
public void setAuditUserName(String auditUserName)
public AuditInput withAuditSingleInput(AuditSingleInput auditSingleInput)
{
this.auditUserName = auditUserName;
}
/*******************************************************************************
** Fluent setter for auditUserName
*******************************************************************************/
public AuditInput withAuditUserName(String auditUserName)
{
this.auditUserName = auditUserName;
return (this);
}
/*******************************************************************************
** Getter for timestamp
*******************************************************************************/
public Instant getTimestamp()
{
return (this.timestamp);
}
/*******************************************************************************
** Setter for timestamp
*******************************************************************************/
public void setTimestamp(Instant timestamp)
{
this.timestamp = timestamp;
}
/*******************************************************************************
** Fluent setter for timestamp
*******************************************************************************/
public AuditInput withTimestamp(Instant timestamp)
{
this.timestamp = timestamp;
return (this);
}
/*******************************************************************************
** Getter for message
*******************************************************************************/
public String getMessage()
{
return (this.message);
}
/*******************************************************************************
** Setter for message
*******************************************************************************/
public void setMessage(String message)
{
this.message = message;
}
/*******************************************************************************
** Fluent setter for message
*******************************************************************************/
public AuditInput withMessage(String message)
{
this.message = message;
return (this);
}
/*******************************************************************************
** Getter for recordIdList
*******************************************************************************/
public List<Integer> getRecordIdList()
{
return (this.recordIdList);
}
/*******************************************************************************
** Setter for recordIdList
*******************************************************************************/
public void setRecordIdList(List<Integer> recordIdList)
{
this.recordIdList = recordIdList;
}
/*******************************************************************************
** Fluent setter for recordIdList
*******************************************************************************/
public AuditInput withRecordIdList(List<Integer> recordIdList)
{
this.recordIdList = recordIdList;
return (this);
}
/*******************************************************************************
** Getter for securityKeyValues
*******************************************************************************/
public Map<String, Serializable> getSecurityKeyValues()
{
return (this.securityKeyValues);
}
/*******************************************************************************
** Setter for securityKeyValues
*******************************************************************************/
public void setSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
}
/*******************************************************************************
** Fluent setter for securityKeyValues
*******************************************************************************/
public AuditInput withSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
addAuditSingleInput(auditSingleInput);
return (this);
}

View File

@ -0,0 +1,303 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
** Input data to insert a single audit record (with optional child record)..
*******************************************************************************/
public class AuditSingleInput
{
private String auditTableName;
private String auditUserName;
private Instant timestamp;
private String message;
private Integer recordId;
private Map<String, Serializable> securityKeyValues;
private List<QRecord> details;
/*******************************************************************************
** Getter for auditTableName
*******************************************************************************/
public String getAuditTableName()
{
return (this.auditTableName);
}
/*******************************************************************************
** Setter for auditTableName
*******************************************************************************/
public void setAuditTableName(String auditTableName)
{
this.auditTableName = auditTableName;
}
/*******************************************************************************
** Fluent setter for auditTableName
*******************************************************************************/
public AuditSingleInput withAuditTableName(String auditTableName)
{
this.auditTableName = auditTableName;
return (this);
}
/*******************************************************************************
** Getter for auditUserName
*******************************************************************************/
public String getAuditUserName()
{
return (this.auditUserName);
}
/*******************************************************************************
** Setter for auditUserName
*******************************************************************************/
public void setAuditUserName(String auditUserName)
{
this.auditUserName = auditUserName;
}
/*******************************************************************************
** Fluent setter for auditUserName
*******************************************************************************/
public AuditSingleInput withAuditUserName(String auditUserName)
{
this.auditUserName = auditUserName;
return (this);
}
/*******************************************************************************
** Getter for timestamp
*******************************************************************************/
public Instant getTimestamp()
{
return (this.timestamp);
}
/*******************************************************************************
** Setter for timestamp
*******************************************************************************/
public void setTimestamp(Instant timestamp)
{
this.timestamp = timestamp;
}
/*******************************************************************************
** Fluent setter for timestamp
*******************************************************************************/
public AuditSingleInput withTimestamp(Instant timestamp)
{
this.timestamp = timestamp;
return (this);
}
/*******************************************************************************
** Getter for message
*******************************************************************************/
public String getMessage()
{
return (this.message);
}
/*******************************************************************************
** Setter for message
*******************************************************************************/
public void setMessage(String message)
{
this.message = message;
}
/*******************************************************************************
** Fluent setter for message
*******************************************************************************/
public AuditSingleInput withMessage(String message)
{
this.message = message;
return (this);
}
/*******************************************************************************
** Getter for securityKeyValues
*******************************************************************************/
public Map<String, Serializable> getSecurityKeyValues()
{
return (this.securityKeyValues);
}
/*******************************************************************************
** Setter for securityKeyValues
*******************************************************************************/
public void setSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
}
/*******************************************************************************
** Fluent setter for securityKeyValues
*******************************************************************************/
public AuditSingleInput withSecurityKeyValues(Map<String, Serializable> securityKeyValues)
{
this.securityKeyValues = securityKeyValues;
return (this);
}
/*******************************************************************************
** Getter for recordId
*******************************************************************************/
public Integer getRecordId()
{
return (this.recordId);
}
/*******************************************************************************
** Setter for recordId
*******************************************************************************/
public void setRecordId(Integer recordId)
{
this.recordId = recordId;
}
/*******************************************************************************
** Fluent setter for recordId
*******************************************************************************/
public AuditSingleInput withRecordId(Integer recordId)
{
this.recordId = recordId;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public AuditSingleInput forRecord(QTableMetaData table, QRecord record)
{
setRecordId(record.getValueInteger(table.getPrimaryKeyField())); // todo support non-integer
setAuditTableName(table.getName());
this.securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName()));
}
return (this);
}
/*******************************************************************************
** Getter for details
*******************************************************************************/
public List<QRecord> getDetails()
{
return (this.details);
}
/*******************************************************************************
** Setter for details
*******************************************************************************/
public void setDetails(List<QRecord> details)
{
this.details = details;
}
/*******************************************************************************
** Fluent setter for details
*******************************************************************************/
public AuditSingleInput withDetails(List<QRecord> details)
{
this.details = details;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void addDetail(String message)
{
if(this.details == null)
{
this.details = new ArrayList<>();
}
QRecord detail = new QRecord().withValue("message", message);
this.details.add(detail);
}
}

View File

@ -0,0 +1,134 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.audits;
import java.io.Serializable;
import java.util.List;
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.data.QRecord;
/*******************************************************************************
** Input object for the DML audit action.
*******************************************************************************/
public class DMLAuditInput extends AbstractActionInput implements Serializable
{
private List<QRecord> recordList;
private List<QRecord> oldRecordList;
private AbstractTableActionInput tableActionInput;
/*******************************************************************************
** Getter for recordList
*******************************************************************************/
public List<QRecord> getRecordList()
{
return (this.recordList);
}
/*******************************************************************************
** Setter for recordList
*******************************************************************************/
public void setRecordList(List<QRecord> recordList)
{
this.recordList = recordList;
}
/*******************************************************************************
** Fluent setter for recordList
*******************************************************************************/
public DMLAuditInput withRecordList(List<QRecord> recordList)
{
this.recordList = recordList;
return (this);
}
/*******************************************************************************
** Getter for tableActionInput
*******************************************************************************/
public AbstractTableActionInput getTableActionInput()
{
return (this.tableActionInput);
}
/*******************************************************************************
** Setter for tableActionInput
*******************************************************************************/
public void setTableActionInput(AbstractTableActionInput tableActionInput)
{
this.tableActionInput = tableActionInput;
}
/*******************************************************************************
** Fluent setter for tableActionInput
*******************************************************************************/
public DMLAuditInput withTableActionInput(AbstractTableActionInput tableActionInput)
{
this.tableActionInput = tableActionInput;
return (this);
}
/*******************************************************************************
** Getter for oldRecordList
*******************************************************************************/
public List<QRecord> getOldRecordList()
{
return (this.oldRecordList);
}
/*******************************************************************************
** Setter for oldRecordList
*******************************************************************************/
public void setOldRecordList(List<QRecord> oldRecordList)
{
this.oldRecordList = oldRecordList;
}
/*******************************************************************************
** Fluent setter for oldRecordList
*******************************************************************************/
public DMLAuditInput withOldRecordList(List<QRecord> oldRecordList)
{
this.oldRecordList = oldRecordList;
return (this);
}
}

View File

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

View File

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

View File

@ -467,4 +467,28 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryLine withSingularMessage(String singularMessage)
{
singularFutureMessage = singularMessage;
singularPastMessage = singularMessage;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public ProcessSummaryLine withPluralMessage(String pluralMessage)
{
pluralFutureMessage = pluralMessage;
pluralPastMessage = pluralMessage;
return (this);
}
}

View File

@ -28,6 +28,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -41,6 +42,8 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
private ProcessState processState;
private Exception exception; // todo - make optional
private List<AuditInput> auditInputList = new ArrayList<>();
/*******************************************************************************
@ -256,4 +259,35 @@ public class RunBackendStepOutput extends AbstractActionOutput implements Serial
this.processState.getRecords().add(record);
}
/*******************************************************************************
** Getter for auditInputList
*******************************************************************************/
public List<AuditInput> getAuditInputList()
{
return (this.auditInputList);
}
/*******************************************************************************
** Setter for auditInputList
*******************************************************************************/
public void setAuditInputList(List<AuditInput> auditInputList)
{
this.auditInputList = auditInputList;
}
/*******************************************************************************
** Fluent setter for auditInputList
*******************************************************************************/
public RunBackendStepOutput withAuditInputList(List<AuditInput> auditInputList)
{
this.auditInputList = auditInputList;
return (this);
}
}

View File

@ -0,0 +1,304 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.code.AdHocScriptCodeReference;
/*******************************************************************************
**
*******************************************************************************/
public class RunAdHocRecordScriptInput extends AbstractTableActionInput
{
private AdHocScriptCodeReference codeReference;
private Map<String, Serializable> inputValues;
private List<Serializable> recordPrimaryKeyList; // can either supply recordList, or recordPrimaryKeyList
private List<QRecord> recordList;
private String tableName;
private QCodeExecutionLoggerInterface logger;
private Serializable outputObject;
private Serializable scriptUtils;
/*******************************************************************************
**
*******************************************************************************/
public RunAdHocRecordScriptInput()
{
}
/*******************************************************************************
** Getter for inputValues
**
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return inputValues;
}
/*******************************************************************************
** Setter for inputValues
**
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
**
*******************************************************************************/
public RunAdHocRecordScriptInput withInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
return (this);
}
/*******************************************************************************
** Getter for outputObject
**
*******************************************************************************/
public Serializable getOutputObject()
{
return outputObject;
}
/*******************************************************************************
** Setter for outputObject
**
*******************************************************************************/
public void setOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
}
/*******************************************************************************
** Fluent setter for outputObject
**
*******************************************************************************/
public RunAdHocRecordScriptInput withOutputObject(Serializable outputObject)
{
this.outputObject = outputObject;
return (this);
}
/*******************************************************************************
** Getter for logger
*******************************************************************************/
public QCodeExecutionLoggerInterface getLogger()
{
return (this.logger);
}
/*******************************************************************************
** Setter for logger
*******************************************************************************/
public void setLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
}
/*******************************************************************************
** Fluent setter for logger
*******************************************************************************/
public RunAdHocRecordScriptInput withLogger(QCodeExecutionLoggerInterface logger)
{
this.logger = logger;
return (this);
}
/*******************************************************************************
** Getter for scriptUtils
**
*******************************************************************************/
public Serializable getScriptUtils()
{
return scriptUtils;
}
/*******************************************************************************
** Setter for scriptUtils
**
*******************************************************************************/
public void setScriptUtils(Serializable scriptUtils)
{
this.scriptUtils = scriptUtils;
}
/*******************************************************************************
** Getter for codeReference
*******************************************************************************/
public AdHocScriptCodeReference getCodeReference()
{
return (this.codeReference);
}
/*******************************************************************************
** Setter for codeReference
*******************************************************************************/
public void setCodeReference(AdHocScriptCodeReference codeReference)
{
this.codeReference = codeReference;
}
/*******************************************************************************
** Fluent setter for codeReference
*******************************************************************************/
public RunAdHocRecordScriptInput withCodeReference(AdHocScriptCodeReference codeReference)
{
this.codeReference = codeReference;
return (this);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public RunAdHocRecordScriptInput withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for recordList
*******************************************************************************/
public List<QRecord> getRecordList()
{
return (this.recordList);
}
/*******************************************************************************
** Setter for recordList
*******************************************************************************/
public void setRecordList(List<QRecord> recordList)
{
this.recordList = recordList;
}
/*******************************************************************************
** Fluent setter for recordList
*******************************************************************************/
public RunAdHocRecordScriptInput withRecordList(List<QRecord> recordList)
{
this.recordList = recordList;
return (this);
}
/*******************************************************************************
** Getter for recordPrimaryKeyList
*******************************************************************************/
public List<Serializable> getRecordPrimaryKeyList()
{
return (this.recordPrimaryKeyList);
}
/*******************************************************************************
** Setter for recordPrimaryKeyList
*******************************************************************************/
public void setRecordPrimaryKeyList(List<Serializable> recordPrimaryKeyList)
{
this.recordPrimaryKeyList = recordPrimaryKeyList;
}
/*******************************************************************************
** Fluent setter for recordPrimaryKeyList
*******************************************************************************/
public RunAdHocRecordScriptInput withRecordPrimaryKeyList(List<Serializable> recordPrimaryKeyList)
{
this.recordPrimaryKeyList = recordPrimaryKeyList;
return (this);
}
}

View File

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

View File

@ -24,15 +24,18 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
import java.io.Serializable;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
/*******************************************************************************
**
** Define an "aggregate", e.g., to be selected in an Aggregate action.
** Such as SUM(cost).
*******************************************************************************/
public class Aggregate implements Serializable
{
private String fieldName;
private AggregateOperator operator;
private QFieldType fieldType;
@ -55,12 +58,14 @@ public class Aggregate implements Serializable
{
return true;
}
if(o == null || getClass() != o.getClass())
{
return false;
}
Aggregate aggregate = (Aggregate) o;
return Objects.equals(fieldName, aggregate.fieldName) && operator == aggregate.operator;
return Objects.equals(fieldName, aggregate.fieldName) && operator == aggregate.operator && fieldType == aggregate.fieldType;
}
@ -71,7 +76,7 @@ public class Aggregate implements Serializable
@Override
public int hashCode()
{
return Objects.hash(fieldName, operator);
return Objects.hash(fieldName, operator, fieldType);
}
@ -153,4 +158,35 @@ public class Aggregate implements Serializable
return (this);
}
/*******************************************************************************
** Getter for fieldType
*******************************************************************************/
public QFieldType getFieldType()
{
return (this.fieldType);
}
/*******************************************************************************
** Setter for fieldType
*******************************************************************************/
public void setFieldType(QFieldType fieldType)
{
this.fieldType = fieldType;
}
/*******************************************************************************
** Fluent setter for fieldType
*******************************************************************************/
public Aggregate withFieldType(QFieldType fieldType)
{
this.fieldType = fieldType;
return (this);
}
}

View File

@ -38,6 +38,7 @@ public class AggregateInput extends AbstractTableActionInput
private QQueryFilter filter;
private List<Aggregate> aggregates;
private List<GroupBy> groupBys = new ArrayList<>();
private Integer limit;
private List<QueryJoin> queryJoins = null;
@ -234,4 +235,38 @@ public class AggregateInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for limit
**
*******************************************************************************/
public Integer getLimit()
{
return limit;
}
/*******************************************************************************
** Setter for limit
**
*******************************************************************************/
public void setLimit(Integer limit)
{
this.limit = limit;
}
/*******************************************************************************
** Fluent setter for limit
**
*******************************************************************************/
public AggregateInput withLimit(Integer limit)
{
this.limit = limit;
return (this);
}
}

View File

@ -27,9 +27,33 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.aggregate;
*******************************************************************************/
public enum AggregateOperator
{
COUNT,
SUM,
MIN,
MAX,
AVG
COUNT("COUNT("),
COUNT_DISTINCT("COUNT(DISTINCT "),
SUM("SUM("),
MIN("MIN("),
MAX("MAX("),
AVG("AVG(");
private final String sqlPrefix;
/*******************************************************************************
**
*******************************************************************************/
AggregateOperator(String sqlPrefix)
{
this.sqlPrefix = sqlPrefix;
}
/*******************************************************************************
** Getter for sqlPrefix
**
*******************************************************************************/
public String getSqlPrefix()
{
return sqlPrefix;
}
}

View File

@ -38,6 +38,17 @@ public class GroupBy implements Serializable
/*******************************************************************************
**
*******************************************************************************/
public GroupBy(QFieldType type, String fieldName)
{
this.type = type;
this.fieldName = fieldName;
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -27,6 +27,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
/*******************************************************************************
@ -101,7 +102,10 @@ public class DeleteInput extends AbstractTableActionInput
*******************************************************************************/
public void setPrimaryKeys(List<Serializable> primaryKeys)
{
this.primaryKeys = primaryKeys;
///////////////////////////////////////////////////////////////////////////////////////////////
// the action may edit this list (e.g., to remove keys w/ errors), so wrap it in MutableList //
///////////////////////////////////////////////////////////////////////////////////////////////
this.primaryKeys = new MutableList<>(primaryKeys);
}
@ -112,7 +116,7 @@ public class DeleteInput extends AbstractTableActionInput
*******************************************************************************/
public DeleteInput withPrimaryKeys(List<Serializable> primaryKeys)
{
this.primaryKeys = primaryKeys;
setPrimaryKeys(primaryKeys);
return (this);
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import java.util.Collection;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -44,6 +45,15 @@ public class GetInput extends AbstractTableActionInput
private boolean shouldFetchHeavyFields = true;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
// if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. //
// to go to a recursive level of associations, you need to dot-qualify the names. e.g., A, B, A.C, A.D, A.C.E //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private boolean includeAssociations = false;
private Collection<String> associationNamesToInclude = null;
/*******************************************************************************
**
@ -229,4 +239,88 @@ public class GetInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for includeAssociations
*******************************************************************************/
public boolean getIncludeAssociations()
{
return (this.includeAssociations);
}
/*******************************************************************************
** Setter for includeAssociations
*******************************************************************************/
public void setIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
}
/*******************************************************************************
** Fluent setter for includeAssociations
*******************************************************************************/
public GetInput withIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
return (this);
}
/*******************************************************************************
** Getter for associationNamesToInclude
*******************************************************************************/
public Collection<String> getAssociationNamesToInclude()
{
return (this.associationNamesToInclude);
}
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
public void setAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
}
/*******************************************************************************
** Fluent setter for associationNamesToInclude
*******************************************************************************/
public GetInput withAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
return (this);
}
/*******************************************************************************
** Fluent setter for shouldTranslatePossibleValues
*******************************************************************************/
public GetInput withShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues)
{
this.shouldTranslatePossibleValues = shouldTranslatePossibleValues;
return (this);
}
/*******************************************************************************
** Fluent setter for shouldGenerateDisplayValues
*******************************************************************************/
public GetInput withShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues)
{
this.shouldGenerateDisplayValues = shouldGenerateDisplayValues;
return (this);
}
}

View File

@ -39,6 +39,8 @@ public class InsertInput extends AbstractTableActionInput
private boolean skipUniqueKeyCheck = false;
private boolean omitDmlAudit = false;
/*******************************************************************************
@ -138,4 +140,46 @@ public class InsertInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for omitDmlAudit
*******************************************************************************/
public boolean getOmitDmlAudit()
{
return (this.omitDmlAudit);
}
/*******************************************************************************
** Setter for omitDmlAudit
*******************************************************************************/
public void setOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
}
/*******************************************************************************
** Fluent setter for omitDmlAudit
*******************************************************************************/
public InsertInput withOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
return (this);
}
/*******************************************************************************
** Fluent setter for records
*******************************************************************************/
public InsertInput withRecords(List<QRecord> records)
{
this.records = records;
return (this);
}
}

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.model.actions.tables.query;
/*******************************************************************************
** Specialization of a QueryJoin, for when the join is added to the query,
** not by the caller, but by the framework, because it is implicitly needed
** to provide a security lock.
*******************************************************************************/
public class ImplicitQueryJoinForSecurityLock extends QueryJoin
{
/*******************************************************************************
** package-private constructor - to make so outside users should not create
** instances.
*******************************************************************************/
ImplicitQueryJoinForSecurityLock()
{
}
}

View File

@ -22,6 +22,8 @@
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -75,25 +77,46 @@ public class JoinsContext
///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
{
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = instance.getTable(mainTableName);
for(String joinName : joinNameChain)
{
if(this.queryJoins.stream().anyMatch(qj -> qj.getJoinMetaData().getName().equals(joinName)))
if(this.queryJoins.stream().anyMatch(queryJoin ->
{
///////////////////////////////////////////////////////
// we're good - we're already joining on this table! //
///////////////////////////////////////////////////////
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
}))
{
continue;
}
QJoinMetaData join = instance.getJoin(joinName);
if(join.getLeftTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
tmpTable = instance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
tmpTable = instance.getTable(join.getLeftTable());
}
else
{
QJoinMetaData join = instance.getJoin(joinName);
if(tableName.equals(join.getRightTable()))
{
join = join.flip();
}
QueryJoin queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
processQueryJoin(queryJoin);
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
}
}
}
@ -297,6 +320,69 @@ public class JoinsContext
/*******************************************************************************
**
*******************************************************************************/
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
{
List<QJoinMetaData> matches = new ArrayList<>();
if(baseTableName != null)
{
///////////////////////////////////////////////////////////////////////////
// if query specified a left-table, look for a join between left & right //
///////////////////////////////////////////////////////////////////////////
for(QJoinMetaData join : instance.getJoins().values())
{
if(join.getLeftTable().equals(baseTableName) && join.getRightTable().equals(joinTableName))
{
matches.add(join);
}
//////////////////////////////
// look in both directions! //
//////////////////////////////
if(join.getRightTable().equals(baseTableName) && join.getLeftTable().equals(joinTableName))
{
matches.add(join.flip());
}
}
}
else
{
/////////////////////////////////////////////////////////////////////////////////////
// if query didn't specify a left-table, then look for any join to the right table //
/////////////////////////////////////////////////////////////////////////////////////
for(QJoinMetaData join : instance.getJoins().values())
{
if(join.getRightTable().equals(joinTableName) && this.hasTable(join.getLeftTable()))
{
matches.add(join);
}
//////////////////////////////
// look in both directions! //
//////////////////////////////
if(join.getLeftTable().equals(joinTableName) && this.hasTable(join.getRightTable()))
{
matches.add(join.flip());
}
}
}
if(matches.size() == 1)
{
return (matches.get(0));
}
else if(matches.size() > 1)
{
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -33,6 +33,8 @@ public enum QCriteriaOperator
IN,
NOT_IN,
IS_NULL_OR_IN,
LIKE,
NOT_LIKE,
STARTS_WITH,
ENDS_WITH,
CONTAINS,

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions.AbstractFilterExpression;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -43,7 +44,8 @@ public class QFilterCriteria implements Serializable, Cloneable
private QCriteriaOperator operator;
private List<Serializable> values;
private String otherFieldName;
private String otherFieldName;
private AbstractFilterExpression<?> expression;
@ -96,6 +98,23 @@ public class QFilterCriteria implements Serializable, Cloneable
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteria(String fieldName, QCriteriaOperator operator, AbstractFilterExpression<?> expression)
{
this.fieldName = fieldName;
this.operator = operator;
this.expression = expression;
///////////////////////////////////////
// this guy doesn't like to be null? //
///////////////////////////////////////
this.values = new ArrayList<>();
}
/*******************************************************************************
**
*******************************************************************************/
@ -188,6 +207,16 @@ public class QFilterCriteria implements Serializable, Cloneable
/*******************************************************************************
**
*******************************************************************************/
public QFilterCriteria withOperator(String operatorName)
{
return withOperator(QCriteriaOperator.valueOf(operatorName.toUpperCase()));
}
/*******************************************************************************
** Getter for values
**
@ -302,4 +331,36 @@ public class QFilterCriteria implements Serializable, Cloneable
return (rs.toString());
}
/*******************************************************************************
** Getter for expression
*******************************************************************************/
public AbstractFilterExpression<?> getExpression()
{
return (this.expression);
}
/*******************************************************************************
** Setter for expression
*******************************************************************************/
public void setExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
}
/*******************************************************************************
** Fluent setter for expression
*******************************************************************************/
public QFilterCriteria withExpression(AbstractFilterExpression<?> expression)
{
this.expression = expression;
return (this);
}
}

View File

@ -26,8 +26,11 @@ import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -363,4 +366,36 @@ public class QQueryFilter implements Serializable, Cloneable
return (rs.toString());
}
/*******************************************************************************
** Replace any criteria values that look like ${input.XXX} with the value of XXX
** from the supplied inputValues map.
**
** Note - it may be very important that you call this method on a clone of a
** QQueryFilter - e.g., if it's one that defined in metaData, and that we don't
** want to be (permanently) changed!!
*******************************************************************************/
public void interpretValues(Map<String, Serializable> inputValues)
{
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", inputValues);
for(QFilterCriteria criterion : getCriteria())
{
if(criterion.getValues() != null)
{
List<Serializable> newValues = new ArrayList<>();
for(Serializable value : criterion.getValues())
{
String valueAsString = ValueUtils.getValueAsString(value);
Serializable interpretedValue = variableInterpreter.interpretForObject(valueAsString);
newValues.add(interpretedValue);
}
criterion.setValues(newValues);
}
}
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -56,6 +57,14 @@ public class QueryInput extends AbstractTableActionInput
private List<QueryJoin> queryJoins = null;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
// if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. //
// to go to a recursive level of associations, you need to dot-qualify the names. e.g., A, B, A.C, A.D, A.C.E //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private boolean includeAssociations = false;
private Collection<String> associationNamesToInclude = null;
/*******************************************************************************
@ -337,4 +346,155 @@ public class QueryInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public QueryInput withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Fluent setter for skip
*******************************************************************************/
public QueryInput withSkip(Integer skip)
{
this.skip = skip;
return (this);
}
/*******************************************************************************
** Fluent setter for limit
*******************************************************************************/
public QueryInput withLimit(Integer limit)
{
this.limit = limit;
return (this);
}
/*******************************************************************************
** Fluent setter for recordPipe
*******************************************************************************/
public QueryInput withRecordPipe(RecordPipe recordPipe)
{
this.recordPipe = recordPipe;
return (this);
}
/*******************************************************************************
** Fluent setter for shouldTranslatePossibleValues
*******************************************************************************/
public QueryInput withShouldTranslatePossibleValues(boolean shouldTranslatePossibleValues)
{
this.shouldTranslatePossibleValues = shouldTranslatePossibleValues;
return (this);
}
/*******************************************************************************
** Fluent setter for shouldGenerateDisplayValues
*******************************************************************************/
public QueryInput withShouldGenerateDisplayValues(boolean shouldGenerateDisplayValues)
{
this.shouldGenerateDisplayValues = shouldGenerateDisplayValues;
return (this);
}
/*******************************************************************************
** Fluent setter for shouldFetchHeavyFields
*******************************************************************************/
public QueryInput withShouldFetchHeavyFields(boolean shouldFetchHeavyFields)
{
this.shouldFetchHeavyFields = shouldFetchHeavyFields;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QueryInput withTableName(String tableName)
{
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
** Getter for includeAssociations
*******************************************************************************/
public boolean getIncludeAssociations()
{
return (this.includeAssociations);
}
/*******************************************************************************
** Setter for includeAssociations
*******************************************************************************/
public void setIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
}
/*******************************************************************************
** Fluent setter for includeAssociations
*******************************************************************************/
public QueryInput withIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
return (this);
}
/*******************************************************************************
** Getter for associationNamesToInclude
*******************************************************************************/
public Collection<String> getAssociationNamesToInclude()
{
return (this.associationNamesToInclude);
}
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
public void setAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
}
/*******************************************************************************
** Fluent setter for associationNamesToInclude
*******************************************************************************/
public QueryInput withAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
return (this);
}
}

View File

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

View File

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

View File

@ -0,0 +1,128 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.actions.tables.query.expressions;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
/*******************************************************************************
**
*******************************************************************************/
public class NowWithOffset extends AbstractFilterExpression<Instant>
{
private final Operator operator;
private final int amount;
private final TimeUnit timeUnit;
public enum Operator
{PLUS, MINUS}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
private NowWithOffset(Operator operator, int amount, TimeUnit timeUnit)
{
this.operator = operator;
this.amount = amount;
this.timeUnit = timeUnit;
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset minus(int amount, TimeUnit timeUnit)
{
return (new NowWithOffset(Operator.MINUS, amount, timeUnit));
}
/*******************************************************************************
** Factory
**
*******************************************************************************/
public static NowWithOffset plus(int amount, TimeUnit timeUnit)
{
return (new NowWithOffset(Operator.PLUS, amount, timeUnit));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public Instant evaluate()
{
if(operator.equals(Operator.PLUS))
{
return (Instant.now().plus(amount, timeUnit.toChronoUnit()));
}
else
{
return (Instant.now().minus(amount, timeUnit.toChronoUnit()));
}
}
/*******************************************************************************
** Getter for operator
**
*******************************************************************************/
public Operator getOperator()
{
return operator;
}
/*******************************************************************************
** Getter for amount
**
*******************************************************************************/
public int getAmount()
{
return amount;
}
/*******************************************************************************
** Getter for timeUnit
**
*******************************************************************************/
public TimeUnit getTimeUnit()
{
return timeUnit;
}
}

View File

@ -45,6 +45,8 @@ public class UpdateInput extends AbstractTableActionInput
////////////////////////////////////////////////////////////////////////////////////////////
private Boolean areAllValuesBeingUpdatedTheSame = null;
private boolean omitDmlAudit = false;
/*******************************************************************************
@ -132,4 +134,57 @@ public class UpdateInput extends AbstractTableActionInput
this.areAllValuesBeingUpdatedTheSame = areAllValuesBeingUpdatedTheSame;
}
/*******************************************************************************
** Getter for omitDmlAudit
*******************************************************************************/
public boolean getOmitDmlAudit()
{
return (this.omitDmlAudit);
}
/*******************************************************************************
** Setter for omitDmlAudit
*******************************************************************************/
public void setOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
}
/*******************************************************************************
** Fluent setter for omitDmlAudit
*******************************************************************************/
public UpdateInput withOmitDmlAudit(boolean omitDmlAudit)
{
this.omitDmlAudit = omitDmlAudit;
return (this);
}
/*******************************************************************************
** Fluent setter for records
*******************************************************************************/
public UpdateInput withRecords(List<QRecord> records)
{
this.records = records;
return (this);
}
/*******************************************************************************
** Fluent setter for areAllValuesBeingUpdatedTheSame
*******************************************************************************/
public UpdateInput withAreAllValuesBeingUpdatedTheSame(Boolean areAllValuesBeingUpdatedTheSame)
{
this.areAllValuesBeingUpdatedTheSame = areAllValuesBeingUpdatedTheSame;
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.values;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -31,7 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
/*******************************************************************************
** Input for the Search possible value source action
*******************************************************************************/
public class SearchPossibleValueSourceInput extends AbstractActionInput
public class SearchPossibleValueSourceInput extends AbstractActionInput implements Cloneable
{
private String possibleValueSourceName;
private QQueryFilter defaultQueryFilter;
@ -52,6 +53,33 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@Override
public SearchPossibleValueSourceInput clone()
{
try
{
SearchPossibleValueSourceInput clone = (SearchPossibleValueSourceInput) super.clone();
if(defaultQueryFilter != null)
{
clone.setDefaultQueryFilter(defaultQueryFilter.clone());
}
if(idList != null)
{
clone.setIdList(new ArrayList<>(idList));
}
return clone;
}
catch(CloneNotSupportedException e)
{
throw new AssertionError();
}
}
/*******************************************************************************
** Getter for possibleValueSourceName
**
@ -253,5 +281,4 @@ public class SearchPossibleValueSourceInput extends AbstractActionInput
this.limit = limit;
return (this);
}
}

View File

@ -27,6 +27,8 @@ import java.util.List;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
@ -34,6 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
@ -43,9 +46,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
*******************************************************************************/
public class AuditsMetaDataProvider
{
public static final String TABLE_NAME_AUDIT_TABLE = "auditTable";
public static final String TABLE_NAME_AUDIT_USER = "auditUser";
public static final String TABLE_NAME_AUDIT = "audit";
public static final String TABLE_NAME_AUDIT_TABLE = "auditTable";
public static final String TABLE_NAME_AUDIT_USER = "auditUser";
public static final String TABLE_NAME_AUDIT = "audit";
public static final String TABLE_NAME_AUDIT_DETAIL = "auditDetail";
@ -80,6 +84,13 @@ public class AuditsMetaDataProvider
.withType(JoinType.MANY_TO_ONE)
.withJoinOn(new JoinOn("auditUserId", "id")));
instance.addJoin(new QJoinMetaData()
.withLeftTable(TABLE_NAME_AUDIT)
.withRightTable(TABLE_NAME_AUDIT_DETAIL)
.withInferredName()
.withType(JoinType.ONE_TO_MANY)
.withJoinOn(new JoinOn("id", "auditId")));
}
@ -111,6 +122,11 @@ public class AuditsMetaDataProvider
.withName(TABLE_NAME_AUDIT_USER)
.withTableName(TABLE_NAME_AUDIT_USER)
);
instance.addPossibleValueSource(new QPossibleValueSource()
.withName(TABLE_NAME_AUDIT)
.withTableName(TABLE_NAME_AUDIT)
);
}
@ -124,6 +140,7 @@ public class AuditsMetaDataProvider
rs.add(enrich(backendDetailEnricher, defineAuditUserTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTableTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditTable(backendName)));
rs.add(enrich(backendDetailEnricher, defineAuditDetailTable(backendName)));
return (rs);
}
@ -151,6 +168,7 @@ public class AuditsMetaDataProvider
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT_TABLE)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("label")
.withPrimaryKeyField("id")
@ -159,7 +177,8 @@ public class AuditsMetaDataProvider
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("label", QFieldType.STRING))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME));
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
@ -172,6 +191,7 @@ public class AuditsMetaDataProvider
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT_USER)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("name")
.withPrimaryKeyField("id")
@ -179,7 +199,8 @@ public class AuditsMetaDataProvider
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("name", QFieldType.STRING))
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME));
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
@ -192,6 +213,7 @@ public class AuditsMetaDataProvider
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s %s")
.withRecordLabelFields("auditTableId", "recordId")
.withPrimaryKeyField("id")
@ -200,7 +222,31 @@ public class AuditsMetaDataProvider
.withField(new QFieldMetaData("auditUserId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT_USER))
.withField(new QFieldMetaData("recordId", QFieldType.INTEGER))
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("timestamp", QFieldType.DATE_TIME));
.withField(new QFieldMetaData("timestamp", QFieldType.DATE_TIME))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
/*******************************************************************************
**
*******************************************************************************/
private QTableMetaData defineAuditDetailTable(String backendName)
{
return new QTableMetaData()
.withName(TABLE_NAME_AUDIT_DETAIL)
.withBackendName(backendName)
.withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE))
.withRecordLabelFormat("%s")
.withRecordLabelFields("id")
.withPrimaryKeyField("id")
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
.withField(new QFieldMetaData("auditId", QFieldType.INTEGER).withPossibleValueSourceName(TABLE_NAME_AUDIT))
.withField(new QFieldMetaData("message", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("fieldName", QFieldType.STRING).withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("oldValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withField(new QFieldMetaData("newValue", QFieldType.STRING).withMaxLength(250).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS))
.withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE);
}
}

View File

@ -0,0 +1,392 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.automation;
import java.time.Instant;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.data.QField;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.tables.TablesPossibleValueSourceMetaDataProvider;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
/*******************************************************************************
** Definition of in-app/user/data-defined triggers (aka, automations).
*******************************************************************************/
public class TableTrigger extends QRecordEntity
{
public static final String TABLE_NAME = "tableTrigger";
@QField(isEditable = false)
private Integer id;
@QField(isEditable = false)
private Instant createDate;
@QField(isEditable = false)
private Instant modifyDate;
@QField(possibleValueSourceName = TablesPossibleValueSourceMetaDataProvider.NAME)
private String tableName;
@QField(/* todo possibleValueSourceName = */)
private Integer filterId;
@QField(possibleValueSourceName = Script.TABLE_NAME)
private Integer scriptId;
@QField()
private Integer priority;
@QField()
private Boolean postInsert;
@QField()
private Boolean postUpdate;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TableTrigger()
{
}
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public TableTrigger(QRecord qRecord) throws QException
{
populateFromQRecord(qRecord);
}
/*******************************************************************************
** Getter for id
**
*******************************************************************************/
public Integer getId()
{
return id;
}
/*******************************************************************************
** Setter for id
**
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Fluent setter for id
**
*******************************************************************************/
public TableTrigger withId(Integer id)
{
this.id = id;
return (this);
}
/*******************************************************************************
** Getter for createDate
**
*******************************************************************************/
public Instant getCreateDate()
{
return createDate;
}
/*******************************************************************************
** Setter for createDate
**
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Fluent setter for createDate
**
*******************************************************************************/
public TableTrigger withCreateDate(Instant createDate)
{
this.createDate = createDate;
return (this);
}
/*******************************************************************************
** Getter for modifyDate
**
*******************************************************************************/
public Instant getModifyDate()
{
return modifyDate;
}
/*******************************************************************************
** Setter for modifyDate
**
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
/*******************************************************************************
** Fluent setter for modifyDate
**
*******************************************************************************/
public TableTrigger withModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
return (this);
}
/*******************************************************************************
** Getter for tableName
**
*******************************************************************************/
public String getTableName()
{
return tableName;
}
/*******************************************************************************
** Setter for tableName
**
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
**
*******************************************************************************/
public TableTrigger withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for filterId
**
*******************************************************************************/
public Integer getFilterId()
{
return filterId;
}
/*******************************************************************************
** Setter for filterId
**
*******************************************************************************/
public void setFilterId(Integer filterId)
{
this.filterId = filterId;
}
/*******************************************************************************
** Fluent setter for filterId
**
*******************************************************************************/
public TableTrigger withFilterId(Integer filterId)
{
this.filterId = filterId;
return (this);
}
/*******************************************************************************
** Getter for scriptId
**
*******************************************************************************/
public Integer getScriptId()
{
return scriptId;
}
/*******************************************************************************
** Setter for scriptId
**
*******************************************************************************/
public void setScriptId(Integer scriptId)
{
this.scriptId = scriptId;
}
/*******************************************************************************
** Fluent setter for scriptId
**
*******************************************************************************/
public TableTrigger withScriptId(Integer scriptId)
{
this.scriptId = scriptId;
return (this);
}
/*******************************************************************************
** Getter for postInsert
**
*******************************************************************************/
public Boolean getPostInsert()
{
return postInsert;
}
/*******************************************************************************
** Setter for postInsert
**
*******************************************************************************/
public void setPostInsert(Boolean postInsert)
{
this.postInsert = postInsert;
}
/*******************************************************************************
** Fluent setter for postInsert
**
*******************************************************************************/
public TableTrigger withPostInsert(Boolean postInsert)
{
this.postInsert = postInsert;
return (this);
}
/*******************************************************************************
** Getter for postUpdate
**
*******************************************************************************/
public Boolean getPostUpdate()
{
return postUpdate;
}
/*******************************************************************************
** Setter for postUpdate
**
*******************************************************************************/
public void setPostUpdate(Boolean postUpdate)
{
this.postUpdate = postUpdate;
}
/*******************************************************************************
** Fluent setter for postUpdate
**
*******************************************************************************/
public TableTrigger withPostUpdate(Boolean postUpdate)
{
this.postUpdate = postUpdate;
return (this);
}
/*******************************************************************************
** Getter for priority
*******************************************************************************/
public Integer getPriority()
{
return (this.priority);
}
/*******************************************************************************
** Setter for priority
*******************************************************************************/
public void setPriority(Integer priority)
{
this.priority = priority;
}
/*******************************************************************************
** Fluent setter for priority
*******************************************************************************/
public TableTrigger withPriority(Integer priority)
{
this.priority = priority;
return (this);
}
}

View File

@ -446,6 +446,17 @@ public class ChartData extends QWidgetData
/*******************************************************************************
** Getter for color
**
*******************************************************************************/
public String getColor()
{
return (this.color);
}
/*******************************************************************************
** Setter for color
**

View File

@ -32,10 +32,11 @@ import java.util.Map;
*******************************************************************************/
public abstract class QWidgetData
{
private String label;
private String footerHTML;
private List<String> dropdownNameList;
private List<String> dropdownLabelList;
private Boolean hasPermission;
/////////////////////////////////////////////////////////////////////////////////////////
// this is a list of lists, the outer list corresponds to each dropdown (parallel list //
@ -88,6 +89,40 @@ public abstract class QWidgetData
/*******************************************************************************
** Getter for footerHTML
**
*******************************************************************************/
public String getFooterHTML()
{
return footerHTML;
}
/*******************************************************************************
** Setter for footerHTML
**
*******************************************************************************/
public void setFooterHTML(String footerHTML)
{
this.footerHTML = footerHTML;
}
/*******************************************************************************
** Fluent setter for footerHTML
**
*******************************************************************************/
public QWidgetData withFooterHTML(String footerHTML)
{
this.footerHTML = footerHTML;
return (this);
}
/*******************************************************************************
** Getter for dropdownLabelList
**
@ -222,4 +257,38 @@ public abstract class QWidgetData
return (this);
}
/*******************************************************************************
** Getter for hasPermission
**
*******************************************************************************/
public Boolean getHasPermission()
{
return hasPermission;
}
/*******************************************************************************
** Setter for hasPermission
**
*******************************************************************************/
public void setHasPermission(Boolean hasPermission)
{
this.hasPermission = hasPermission;
}
/*******************************************************************************
** Fluent setter for hasPermission
**
*******************************************************************************/
public QWidgetData withHasPermission(Boolean hasPermission)
{
this.hasPermission = hasPermission;
return (this);
}
}

View File

@ -38,6 +38,15 @@ public class StatisticsData extends QWidgetData
/*******************************************************************************
**
*******************************************************************************/
public StatisticsData()
{
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -37,19 +37,28 @@ public class TableData extends QWidgetData
private String noRowsFoundHTML;
private List<Column> columns;
private List<Map<String, Object>> rows;
private List<Map<String, String>> dropdownOptions;
private Integer rowsPerPage;
private Boolean hidePaginationDropdown;
/*******************************************************************************
**
*******************************************************************************/
public TableData(String label, List<Column> columns, List<Map<String, Object>> rows, List<Map<String, String>> dropdownOptions)
public TableData()
{
}
/*******************************************************************************
**
*******************************************************************************/
public TableData(String label, List<Column> columns, List<Map<String, Object>> rows)
{
setLabel(label);
setColumns(columns);
setRows(rows);
setDropdownOptions(dropdownOptions);
}
@ -65,6 +74,74 @@ public class TableData extends QWidgetData
/*******************************************************************************
** Getter for rowsPerPage
**
*******************************************************************************/
public Integer getRowsPerPage()
{
return rowsPerPage;
}
/*******************************************************************************
** Setter for rowsPerPage
**
*******************************************************************************/
public void setRowsPerPage(Integer rowsPerPage)
{
this.rowsPerPage = rowsPerPage;
}
/*******************************************************************************
** Fluent setter for rowsPerPage
**
*******************************************************************************/
public TableData withRowsPerPage(Integer rowsPerPage)
{
this.rowsPerPage = rowsPerPage;
return (this);
}
/*******************************************************************************
** Getter for hidePaginationDropdown
**
*******************************************************************************/
public Boolean getHidePaginationDropdown()
{
return hidePaginationDropdown;
}
/*******************************************************************************
** Setter for hidePaginationDropdown
**
*******************************************************************************/
public void setHidePaginationDropdown(Boolean hidePaginationDropdown)
{
this.hidePaginationDropdown = hidePaginationDropdown;
}
/*******************************************************************************
** Fluent setter for hidePaginationDropdown
**
*******************************************************************************/
public TableData withHidePaginationDropdown(Boolean hidePaginationDropdown)
{
this.hidePaginationDropdown = hidePaginationDropdown;
return (this);
}
/*******************************************************************************
** Getter for columns
**
@ -133,40 +210,6 @@ public class TableData extends QWidgetData
/*******************************************************************************
** Getter for dropdownOptions
**
*******************************************************************************/
public List<Map<String, String>> getDropdownOptions()
{
return dropdownOptions;
}
/*******************************************************************************
** Setter for dropdownOptions
**
*******************************************************************************/
public void setDropdownOptions(List<Map<String, String>> dropdownOptions)
{
this.dropdownOptions = dropdownOptions;
}
/*******************************************************************************
** Fluent setter for dropdownOptions
**
*******************************************************************************/
public TableData withDropdownOptions(List<Map<String, String>> dropdownOptions)
{
this.dropdownOptions = dropdownOptions;
return (this);
}
/*******************************************************************************
** Getter for noRowsFoundHTML
**

View File

@ -44,12 +44,12 @@ public enum WidgetType
PROCESS("process"),
QUICK_SIGHT_CHART("quickSightChart"),
STATISTICS("statistics"),
SIMPLE_STATISTICS("simpleStatistics"),
STACKED_BAR_CHART("stackedBarChart"),
STEPPER("stepper"),
TABLE("table"),
USA_MAP("usaMap"),
DATA_BAG_VIEWER("dataBagViewer");
DATA_BAG_VIEWER("dataBagViewer"),
SCRIPT_VIEWER("scriptViewer");
private final String type;

View File

@ -28,6 +28,7 @@ import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@ -65,6 +66,8 @@ public class QRecord implements Serializable
private Map<String, Serializable> backendDetails = new LinkedHashMap<>();
private List<String> errors = new ArrayList<>();
private Map<String, List<QRecord>> associatedRecords = new HashMap<>();
public static final String BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT = "jsonSourceObject";
@ -102,6 +105,7 @@ public class QRecord implements Serializable
this.displayValues = doDeepCopy(record.displayValues);
this.backendDetails = doDeepCopy(record.backendDetails);
this.errors = doDeepCopy(record.errors);
this.associatedRecords = doDeepCopy(record.associatedRecords);
}
@ -159,6 +163,29 @@ public class QRecord implements Serializable
/*******************************************************************************
** Added when QRecords got exposed in scripts, and passing a constant String
** raised a class-cast exception, because it was some nashorn non-serializable
** type (though once inside *this* method, the value was a java.lang.String...)
*******************************************************************************/
public void setValue(String fieldName, Object value)
{
if(value == null)
{
setValue(fieldName, null);
}
else if(value instanceof Serializable s)
{
setValue(fieldName, s);
}
else
{
setValue(fieldName, ValueUtils.getValueAsString(value));
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -552,4 +579,66 @@ public class QRecord implements Serializable
return (QRecordEntity.fromQRecord(c, this));
}
/*******************************************************************************
** Getter for associatedRecords
*******************************************************************************/
public Map<String, List<QRecord>> getAssociatedRecords()
{
return (this.associatedRecords);
}
/*******************************************************************************
** Setter for associatedRecords
*******************************************************************************/
public void setAssociatedRecords(Map<String, List<QRecord>> associatedRecords)
{
this.associatedRecords = associatedRecords;
}
/*******************************************************************************
** Fluent setter for associatedRecords
*******************************************************************************/
public QRecord withAssociatedRecords(Map<String, List<QRecord>> associatedRecords)
{
this.associatedRecords = associatedRecords;
return (this);
}
/*******************************************************************************
** Fluent setter for associatedRecords
*******************************************************************************/
public QRecord withAssociatedRecords(String name, List<QRecord> associatedRecords)
{
if(this.associatedRecords == null)
{
this.associatedRecords = new HashMap<>();
}
this.associatedRecords.put(name, associatedRecords);
return (this);
}
/*******************************************************************************
** Fluent setter for associatedRecord
*******************************************************************************/
public QRecord withAssociatedRecord(String name, QRecord associatedRecord)
{
if(this.associatedRecords == null)
{
this.associatedRecords = new HashMap<>();
}
this.associatedRecords.putIfAbsent(name, new ArrayList<>());
this.associatedRecords.get(name).add(associatedRecord);
return (this);
}
}

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.model.metadata;
import com.kingsrook.qqq.backend.core.exceptions.QException;
/*******************************************************************************
** Abstract class that knows how to produce meta data objects. Useful with
** MetaDataProducerHelper, to put point at a package full of these, and populate
** your whole QInstance.
*******************************************************************************/
public abstract class MetaDataProducer<T extends TopLevelMetaDataInterface>
{
/*******************************************************************************
** Produce the metaData object. Generally, you don't want to add it to the instance
** yourself - but the instance is there in case you need it to get other metaData.
*******************************************************************************/
public abstract T produce(QInstance qInstance) throws QException;
/*******************************************************************************
** In case this producer needs to run before (or after) others, this method
** can help influence that (e.g., if used by MetaDataProducerHelper).
*******************************************************************************/
public int getSortOrder()
{
return (500);
}
}

View File

@ -0,0 +1,134 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import com.google.common.reflect.ClassPath;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Help work with MetaDataProducers.
*******************************************************************************/
public class MetaDataProducerHelper
{
private static final QLogger LOG = QLogger.getLogger(MetaDataProducerHelper.class);
/*******************************************************************************
** Recursively find all classes in the given package, that extend MetaDataProducer,
** run them, and add their output to the given qInstance.
**
** Note - they'll be sorted by the sortOrder they provide.
*******************************************************************************/
public static void processAllMetaDataProducersInPackage(QInstance instance, String packageName) throws IOException
{
////////////////////////////////////////////////////////////
// find all the meta data producer classes in the package //
////////////////////////////////////////////////////////////
List<Class<?>> classesInPackage = getClassesInPackage(packageName);
List<MetaDataProducer<?>> producers = new ArrayList<>();
for(Class<?> aClass : classesInPackage)
{
try
{
if(Modifier.isAbstract(aClass.getModifiers()))
{
continue;
}
for(Constructor<?> constructor : aClass.getConstructors())
{
if(constructor.getParameterCount() == 0)
{
Object o = constructor.newInstance();
if(o instanceof MetaDataProducer<?> metaDataProducer)
{
producers.add(metaDataProducer);
}
break;
}
}
}
catch(Exception e)
{
LOG.info("Error adding metaData from producer", logPair("producer", aClass.getSimpleName()), e);
}
}
/////////////////////////////
// sort them by sort order //
/////////////////////////////
producers.sort(Comparator.comparing(p -> p.getSortOrder()));
//////////////////////////////////////////////////////////////
// execute each one, adding their meta data to the instance //
//////////////////////////////////////////////////////////////
for(MetaDataProducer<?> producer : producers)
{
try
{
TopLevelMetaDataInterface metaData = producer.produce(instance);
if(metaData != null)
{
metaData.addSelfToInstance(instance);
}
}
catch(Exception e)
{
LOG.warn("error executing metaDataProducer", logPair("producer", producer.getClass().getSimpleName()), e);
}
}
}
/*******************************************************************************
** from https://stackoverflow.com/questions/520328/can-you-find-all-classes-in-a-package-using-reflection
** (since the original, from ChatGPT, didn't work in jars, despite GPT hallucinating that it would)
*******************************************************************************/
private static List<Class<?>> getClassesInPackage(String packageName) throws IOException
{
List<Class<?>> classes = new ArrayList<>();
ClassLoader loader = Thread.currentThread().getContextClassLoader();
for(ClassPath.ClassInfo info : ClassPath.from(loader).getTopLevelClasses())
{
if(info.getName().startsWith(packageName))
{
classes.add(info.load());
}
}
return (classes);
}
}

View File

@ -36,6 +36,7 @@ import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
@ -89,9 +90,13 @@ public class QInstance
private Map<String, QQueueProviderMetaData> queueProviders = new LinkedHashMap<>();
private Map<String, QQueueMetaData> queues = new LinkedHashMap<>();
private Map<String, QMiddlewareInstanceMetaData> middlewareMetaData = new LinkedHashMap<>();
private Map<String, String> environmentValues = new LinkedHashMap<>();
private String defaultTimeZoneId = "UTC";
private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance();
private QAuditRules defaultAuditRules = QAuditRules.defaultInstanceLevelNone();
// todo - lock down the object (no more changes allowed) after it's been validated?
@ -188,7 +193,7 @@ public class QInstance
/*******************************************************************************
** Get the full path to a table
*******************************************************************************/
public String getTablePath(AbstractActionInput actionInput, String tableName) throws QException
public String getTablePath(String tableName) throws QException
{
if(!memoizedTablePaths.containsKey(tableName))
{
@ -993,6 +998,37 @@ public class QInstance
/*******************************************************************************
** Setter for defaultTimeZoneId
*******************************************************************************/
public void setDefaultTimeZoneId(String defaultTimeZoneId)
{
this.defaultTimeZoneId = defaultTimeZoneId;
}
/*******************************************************************************
** Fluent setter for defaultTimeZoneId
*******************************************************************************/
public QInstance withDefaultTimeZoneId(String defaultTimeZoneId)
{
this.defaultTimeZoneId = defaultTimeZoneId;
return (this);
}
/*******************************************************************************
** Getter for defaultTimeZoneId
*******************************************************************************/
public String getDefaultTimeZoneId()
{
return (this.defaultTimeZoneId);
}
/*******************************************************************************
**
*******************************************************************************/
@ -1010,4 +1046,94 @@ public class QInstance
return (rs);
}
/*******************************************************************************
** Getter for defaultAuditRules
*******************************************************************************/
public QAuditRules getDefaultAuditRules()
{
return (this.defaultAuditRules);
}
/*******************************************************************************
** Setter for defaultAuditRules
*******************************************************************************/
public void setDefaultAuditRules(QAuditRules defaultAuditRules)
{
this.defaultAuditRules = defaultAuditRules;
}
/*******************************************************************************
** Fluent setter for defaultAuditRules
*******************************************************************************/
public QInstance withDefaultAuditRules(QAuditRules defaultAuditRules)
{
this.defaultAuditRules = defaultAuditRules;
return (this);
}
/*******************************************************************************
** Getter for middlewareMetaData
*******************************************************************************/
public Map<String, QMiddlewareInstanceMetaData> getMiddlewareMetaData()
{
return (this.middlewareMetaData);
}
/*******************************************************************************
** Getter for middlewareMetaData
*******************************************************************************/
public QMiddlewareInstanceMetaData getMiddlewareMetaData(String type)
{
if(this.middlewareMetaData == null)
{
return (null);
}
return this.middlewareMetaData.get(type);
}
/*******************************************************************************
** Setter for middlewareMetaData
*******************************************************************************/
public void setMiddlewareMetaData(Map<String, QMiddlewareInstanceMetaData> middlewareMetaData)
{
this.middlewareMetaData = middlewareMetaData;
}
/*******************************************************************************
** Fluent setter for middlewareMetaData
*******************************************************************************/
public QInstance withMiddlewareMetaData(Map<String, QMiddlewareInstanceMetaData> middlewareMetaData)
{
this.middlewareMetaData = middlewareMetaData;
return (this);
}
/*******************************************************************************
** Fluent setter for middlewareMetaData
*******************************************************************************/
public QInstance withMiddlewareMetaData(QMiddlewareInstanceMetaData middlewareMetaData)
{
if(this.middlewareMetaData == null)
{
this.middlewareMetaData = new HashMap<>();
}
this.middlewareMetaData.put(middlewareMetaData.getType(), middlewareMetaData);
return (this);
}
}

View File

@ -0,0 +1,90 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Base-class for instance-level meta-data defined for a specific middleware.
*******************************************************************************/
public abstract class QMiddlewareInstanceMetaData
{
protected String type;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public QMiddlewareInstanceMetaData withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void enrich(QTableMetaData table)
{
////////////////////////
// noop in base class //
////////////////////////
}
/*******************************************************************************
**
*******************************************************************************/
public void validate(QInstance qInstance, QInstanceValidator validator)
{
////////////////////////
// noop in base class //
////////////////////////
}
}

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.authentication;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
@ -43,6 +44,31 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
@JsonIgnore
private String clientSecret;
///////////////////////////////////////////////////////////////////////////////////////////
// these tables and fields are used to store auth0 application data and access data, the //
// access token can potentially be too large to send to qqq because of size limiations, //
// so we need to hash it and send the qqq user a version mapped to a smaller token //
///////////////////////////////////////////////////////////////////////////////////////////
private String clientAuth0ApplicationTableName;
private String accessTokenTableName;
/////////////////////////////////////////
// fields on the auth0ApplicationTable //
/////////////////////////////////////////
private String applicationNameField;
private String auth0ClientIdField;
private String auth0ClientSecretMaskedField;
private Serializable qqqRecordIdField;
/////////////////////////////////////
// fields on the accessToken table //
/////////////////////////////////////
private String clientAuth0ApplicationIdField;
private String auth0AccessTokenField;
private String qqqAccessTokenField;
private String expiresInSecondsField;
/*******************************************************************************
@ -189,4 +215,344 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
return (this);
}
/*******************************************************************************
** Getter for clientAuth0ApplicationTableName
**
*******************************************************************************/
public String getClientAuth0ApplicationTableName()
{
return clientAuth0ApplicationTableName;
}
/*******************************************************************************
** Setter for clientAuth0ApplicationTableName
**
*******************************************************************************/
public void setClientAuth0ApplicationTableName(String clientAuth0ApplicationTableName)
{
this.clientAuth0ApplicationTableName = clientAuth0ApplicationTableName;
}
/*******************************************************************************
** Fluent setter for clientAuth0ApplicationTableName
**
*******************************************************************************/
public Auth0AuthenticationMetaData withClientAuth0ApplicationTableName(String clientAuth0ApplicationTableName)
{
this.clientAuth0ApplicationTableName = clientAuth0ApplicationTableName;
return (this);
}
/*******************************************************************************
** Getter for accessTokenTableName
**
*******************************************************************************/
public String getAccessTokenTableName()
{
return accessTokenTableName;
}
/*******************************************************************************
** Setter for accessTokenTableName
**
*******************************************************************************/
public void setAccessTokenTableName(String accessTokenTableName)
{
this.accessTokenTableName = accessTokenTableName;
}
/*******************************************************************************
** Fluent setter for accessTokenTableName
**
*******************************************************************************/
public Auth0AuthenticationMetaData withAccessTokenTableName(String accessTokenTableName)
{
this.accessTokenTableName = accessTokenTableName;
return (this);
}
/*******************************************************************************
** Getter for applicationNameField
**
*******************************************************************************/
public String getApplicationNameField()
{
return applicationNameField;
}
/*******************************************************************************
** Setter for applicationNameField
**
*******************************************************************************/
public void setApplicationNameField(String applicationNameField)
{
this.applicationNameField = applicationNameField;
}
/*******************************************************************************
** Fluent setter for applicationNameField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withApplicationNameField(String applicationNameField)
{
this.applicationNameField = applicationNameField;
return (this);
}
/*******************************************************************************
** Getter for auth0ClientIdField
**
*******************************************************************************/
public String getAuth0ClientIdField()
{
return auth0ClientIdField;
}
/*******************************************************************************
** Setter for auth0ClientIdField
**
*******************************************************************************/
public void setAuth0ClientIdField(String auth0ClientIdField)
{
this.auth0ClientIdField = auth0ClientIdField;
}
/*******************************************************************************
** Fluent setter for auth0ClientIdField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withAuth0ClientIdField(String auth0ClientIdField)
{
this.auth0ClientIdField = auth0ClientIdField;
return (this);
}
/*******************************************************************************
** Getter for qqqRecordIdField
**
*******************************************************************************/
public Serializable getQqqRecordIdField()
{
return qqqRecordIdField;
}
/*******************************************************************************
** Setter for qqqRecordIdField
**
*******************************************************************************/
public void setQqqRecordIdField(Serializable qqqRecordIdField)
{
this.qqqRecordIdField = qqqRecordIdField;
}
/*******************************************************************************
** Fluent setter for qqqRecordIdField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withQqqRecordIdField(Serializable qqqRecordIdField)
{
this.qqqRecordIdField = qqqRecordIdField;
return (this);
}
/*******************************************************************************
** Getter for auth0ClientSecretMaskedField
**
*******************************************************************************/
public String getAuth0ClientSecretMaskedField()
{
return auth0ClientSecretMaskedField;
}
/*******************************************************************************
** Setter for auth0ClientSecretMaskedField
**
*******************************************************************************/
public void setAuth0ClientSecretMaskedField(String auth0ClientSecretMaskedField)
{
this.auth0ClientSecretMaskedField = auth0ClientSecretMaskedField;
}
/*******************************************************************************
** Fluent setter for auth0ClientSecretMaskedField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withAuth0ClientSecretMaskedField(String auth0ClientSecretMaskedField)
{
this.auth0ClientSecretMaskedField = auth0ClientSecretMaskedField;
return (this);
}
/*******************************************************************************
** Getter for clientAuth0ApplicationIdField
**
*******************************************************************************/
public String getClientAuth0ApplicationIdField()
{
return clientAuth0ApplicationIdField;
}
/*******************************************************************************
** Setter for clientAuth0ApplicationIdField
**
*******************************************************************************/
public void setClientAuth0ApplicationIdField(String clientAuth0ApplicationIdField)
{
this.clientAuth0ApplicationIdField = clientAuth0ApplicationIdField;
}
/*******************************************************************************
** Fluent setter for clientAuth0ApplicationIdField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withClientAuth0ApplicationIdField(String clientAuth0ApplicationIdField)
{
this.clientAuth0ApplicationIdField = clientAuth0ApplicationIdField;
return (this);
}
/*******************************************************************************
** Getter for auth0AccessTokenField
**
*******************************************************************************/
public String getAuth0AccessTokenField()
{
return auth0AccessTokenField;
}
/*******************************************************************************
** Setter for auth0AccessTokenField
**
*******************************************************************************/
public void setAuth0AccessTokenField(String auth0AccessTokenField)
{
this.auth0AccessTokenField = auth0AccessTokenField;
}
/*******************************************************************************
** Fluent setter for auth0AccessTokenField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withAuth0AccessTokenField(String auth0AccessTokenField)
{
this.auth0AccessTokenField = auth0AccessTokenField;
return (this);
}
/*******************************************************************************
** Getter for qqqAccessTokenField
**
*******************************************************************************/
public String getQqqAccessTokenField()
{
return qqqAccessTokenField;
}
/*******************************************************************************
** Setter for qqqAccessTokenField
**
*******************************************************************************/
public void setQqqAccessTokenField(String qqqAccessTokenField)
{
this.qqqAccessTokenField = qqqAccessTokenField;
}
/*******************************************************************************
** Fluent setter for qqqAccessTokenField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withQqqAccessTokenField(String qqqAccessTokenField)
{
this.qqqAccessTokenField = qqqAccessTokenField;
return (this);
}
/*******************************************************************************
** Getter for expiresInSecondsField
**
*******************************************************************************/
public String getExpiresInSecondsField()
{
return expiresInSecondsField;
}
/*******************************************************************************
** Setter for expiresInSecondsField
**
*******************************************************************************/
public void setExpiresInSecondsField(String expiresInSecondsField)
{
this.expiresInSecondsField = expiresInSecondsField;
}
/*******************************************************************************
** Fluent setter for expiresInSecondsField
**
*******************************************************************************/
public Auth0AuthenticationMetaData withExpiresInSecondsField(String expiresInSecondsField)
{
this.expiresInSecondsField = expiresInSecondsField;
return (this);
}
}

View File

@ -29,8 +29,11 @@ package com.kingsrook.qqq.backend.core.model.metadata.branding;
public class QBrandingMetaData
{
private String companyName;
private String companyUrl;
private String appName;
private String logo;
private String icon;
private String accentColor;
@ -40,7 +43,7 @@ public class QBrandingMetaData
@Override
public String toString()
{
return ("QBrandingMetaData[" + companyName + "]");
return ("QBrandingMetaData[" + appName + "]");
}
@ -145,4 +148,100 @@ public class QBrandingMetaData
return this;
}
/*******************************************************************************
** Getter for appName
*******************************************************************************/
public String getAppName()
{
return (this.appName);
}
/*******************************************************************************
** Setter for appName
*******************************************************************************/
public void setAppName(String appName)
{
this.appName = appName;
}
/*******************************************************************************
** Fluent setter for appName
*******************************************************************************/
public QBrandingMetaData withAppName(String appName)
{
this.appName = appName;
return (this);
}
/*******************************************************************************
** Getter for companyUrl
*******************************************************************************/
public String getCompanyUrl()
{
return (this.companyUrl);
}
/*******************************************************************************
** Setter for companyUrl
*******************************************************************************/
public void setCompanyUrl(String companyUrl)
{
this.companyUrl = companyUrl;
}
/*******************************************************************************
** Fluent setter for companyUrl
*******************************************************************************/
public QBrandingMetaData withCompanyUrl(String companyUrl)
{
this.companyUrl = companyUrl;
return (this);
}
/*******************************************************************************
** Getter for accentColor
**
*******************************************************************************/
public String getAccentColor()
{
return accentColor;
}
/*******************************************************************************
** Setter for accentColor
**
*******************************************************************************/
public void setAccentColor(String accentColor)
{
this.accentColor = accentColor;
}
/*******************************************************************************
** Fluent setter for accentColor
**
*******************************************************************************/
public QBrandingMetaData withAccentColor(String accentColor)
{
this.accentColor = accentColor;
return (this);
}
}

View File

@ -0,0 +1,145 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.code;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
/*******************************************************************************
**
*******************************************************************************/
public class AdHocScriptCodeReference extends QCodeReference
{
////////////////////////////////////////////////////////////////////////////////
// can supply scriptId (in which case, current revisionId will be looked up), //
// or revisionId (in which case, record will be looked up) //
// or, the record. //
////////////////////////////////////////////////////////////////////////////////
private Integer scriptId;
private Integer scriptRevisionId;
private QRecord scriptRevisionRecord;
/*******************************************************************************
** Getter for scriptId
*******************************************************************************/
public Integer getScriptId()
{
return (this.scriptId);
}
/*******************************************************************************
** Setter for scriptId
*******************************************************************************/
public void setScriptId(Integer scriptId)
{
this.scriptId = scriptId;
}
/*******************************************************************************
** Fluent setter for scriptId
*******************************************************************************/
public AdHocScriptCodeReference withScriptId(Integer scriptId)
{
this.scriptId = scriptId;
return (this);
}
/*******************************************************************************
** Getter for scriptRevisionId
*******************************************************************************/
public Integer getScriptRevisionId()
{
return (this.scriptRevisionId);
}
/*******************************************************************************
** Setter for scriptRevisionId
*******************************************************************************/
public void setScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
}
/*******************************************************************************
** Fluent setter for scriptRevisionId
*******************************************************************************/
public AdHocScriptCodeReference withScriptRevisionId(Integer scriptRevisionId)
{
this.scriptRevisionId = scriptRevisionId;
return (this);
}
/*******************************************************************************
** Getter for scriptRevisionRecord
*******************************************************************************/
public QRecord getScriptRevisionRecord()
{
return (this.scriptRevisionRecord);
}
/*******************************************************************************
** Setter for scriptRevisionRecord
*******************************************************************************/
public void setScriptRevisionRecord(QRecord scriptRevisionRecord)
{
this.scriptRevisionRecord = scriptRevisionRecord;
}
/*******************************************************************************
** Fluent setter for scriptRevisionRecord
*******************************************************************************/
public AdHocScriptCodeReference withScriptRevisionRecord(QRecord scriptRevisionRecord)
{
this.scriptRevisionRecord = scriptRevisionRecord;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "AdHocScriptCodeReference{scriptId=" + scriptId + ", scriptRevisionId=" + scriptRevisionId + ", scriptRevisionRecord=" + scriptRevisionRecord + '}';
}
}

View File

@ -167,4 +167,16 @@ public class AssociatedScriptCodeReference extends QCodeReference
{
return Objects.hash(recordTable, recordPrimaryKey, fieldName);
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "AssociatedScriptCodeReference{recordTable='" + recordTable + '\'' + ", recordPrimaryKey=" + recordPrimaryKey + ", fieldName='" + fieldName + '\'' + '}';
}
}

View File

@ -41,6 +41,8 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
protected String icon;
protected String label;
protected String type;
protected String minHeight;
protected String footerHTML;
protected boolean isCard;
protected Integer gridColumns;
protected QCodeReference codeReference;
@ -428,6 +430,40 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
/*******************************************************************************
** Getter for minHeight
**
*******************************************************************************/
public String getMinHeight()
{
return minHeight;
}
/*******************************************************************************
** Setter for minHeight
**
*******************************************************************************/
public void setMinHeight(String minHeight)
{
this.minHeight = minHeight;
}
/*******************************************************************************
** Fluent setter for minHeight
**
*******************************************************************************/
public QWidgetMetaData withMinHeight(String minHeight)
{
this.minHeight = minHeight;
return (this);
}
/*******************************************************************************
** Getter for permissionRules
*******************************************************************************/
@ -459,4 +495,38 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface
return (this);
}
/*******************************************************************************
** Getter for footerHTML
**
*******************************************************************************/
public String getFooterHTML()
{
return footerHTML;
}
/*******************************************************************************
** Setter for footerHTML
**
*******************************************************************************/
public void setFooterHTML(String footerHTML)
{
this.footerHTML = footerHTML;
}
/*******************************************************************************
** Fluent setter for footerHTML
**
*******************************************************************************/
public QWidgetMetaData withFooterHTML(String footerHTML)
{
this.footerHTML = footerHTML;
return (this);
}
}

View File

@ -51,6 +51,16 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules
*******************************************************************************/
QWidgetMetaDataInterface withName(String name);
/*******************************************************************************
** Getter for footerHTML
*******************************************************************************/
String getFooterHTML();
/*******************************************************************************
** Setter for footerHTML
*******************************************************************************/
void setFooterHTML(String label);
/*******************************************************************************
** Getter for label
*******************************************************************************/
@ -131,6 +141,15 @@ public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules
*******************************************************************************/
void setIsCard(boolean isCard);
/*******************************************************************************
** Getter for minHeight
*******************************************************************************/
String getMinHeight();
/*******************************************************************************
** Setter for minHeight
*******************************************************************************/
void setMinHeight(String minHeight);
/*******************************************************************************
** Getter for storeDropdownSelections

View File

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

View File

@ -0,0 +1,107 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetOutput
{
protected QFilterCriteria condition;
protected String type;
/*******************************************************************************
**
*******************************************************************************/
public abstract String render(Map<String, Object> context) throws QException;
/*******************************************************************************
** Getter for condition
*******************************************************************************/
public QFilterCriteria getCondition()
{
return (this.condition);
}
/*******************************************************************************
** Setter for condition
*******************************************************************************/
public void setCondition(QFilterCriteria condition)
{
this.condition = condition;
}
/*******************************************************************************
** Fluent setter for condition
*******************************************************************************/
public AbstractWidgetOutput withCondition(QFilterCriteria condition)
{
this.condition = condition;
return (this);
}
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public AbstractWidgetOutput withType(String type)
{
this.type = type;
return (this);
}
}

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.model.metadata.dashboard.nocode;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetValueSource
{
protected String name;
protected String type;
protected Map<String, Serializable> inputValues;
/*******************************************************************************
**
*******************************************************************************/
public abstract Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException;
/*******************************************************************************
** Getter for type
*******************************************************************************/
public String getType()
{
return (this.type);
}
/*******************************************************************************
** Setter for type
*******************************************************************************/
public void setType(String type)
{
this.type = type;
}
/*******************************************************************************
** Fluent setter for type
*******************************************************************************/
public AbstractWidgetValueSource withType(String type)
{
this.type = type;
return (this);
}
/*******************************************************************************
** Getter for name
*******************************************************************************/
public String getName()
{
return (this.name);
}
/*******************************************************************************
** Setter for name
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Fluent setter for name
*******************************************************************************/
public AbstractWidgetValueSource withName(String name)
{
this.name = name;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void supplementContext(Map<String, Object> context)
{
////////////////////////
// noop in base class //
////////////////////////
}
/*******************************************************************************
** Getter for inputValues
*******************************************************************************/
public Map<String, Serializable> getInputValues()
{
return (this.inputValues);
}
/*******************************************************************************
** Setter for inputValues
*******************************************************************************/
public void setInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
}
/*******************************************************************************
** Fluent setter for inputValues
*******************************************************************************/
public AbstractWidgetValueSource withInputValues(Map<String, Serializable> inputValues)
{
this.inputValues = inputValues;
return (this);
}
}

View File

@ -0,0 +1,167 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.util.List;
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.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
/*******************************************************************************
**
*******************************************************************************/
public abstract class AbstractWidgetValueSourceWithFilter extends AbstractWidgetValueSource
{
protected String tableName;
protected QQueryFilter filter;
protected List<AbstractConditionalFilter> conditionalFilterList;
/*******************************************************************************
**
*******************************************************************************/
public QQueryFilter getEffectiveFilter(RenderWidgetInput input)
{
QQueryFilter effectiveFilter;
if(filter == null)
{
effectiveFilter = new QQueryFilter();
}
else
{
effectiveFilter = filter.clone();
}
for(AbstractConditionalFilter conditionalFilter : CollectionUtils.nonNullList(conditionalFilterList))
{
if(conditionalFilter.testCondition(input))
{
QQueryFilter additionalFilter = conditionalFilter.getFilter(input);
for(QFilterCriteria criterion : additionalFilter.getCriteria())
{
effectiveFilter.addCriteria(criterion);
}
}
}
return (effectiveFilter);
}
/*******************************************************************************
** Getter for tableName
*******************************************************************************/
public String getTableName()
{
return (this.tableName);
}
/*******************************************************************************
** Setter for tableName
*******************************************************************************/
public void setTableName(String tableName)
{
this.tableName = tableName;
}
/*******************************************************************************
** Fluent setter for tableName
*******************************************************************************/
public AbstractWidgetValueSourceWithFilter withTableName(String tableName)
{
this.tableName = tableName;
return (this);
}
/*******************************************************************************
** Getter for filter
*******************************************************************************/
public QQueryFilter getFilter()
{
return (this.filter);
}
/*******************************************************************************
** Setter for filter
*******************************************************************************/
public void setFilter(QQueryFilter filter)
{
this.filter = filter;
}
/*******************************************************************************
** Fluent setter for filter
*******************************************************************************/
public AbstractWidgetValueSourceWithFilter withFilter(QQueryFilter filter)
{
this.filter = filter;
return (this);
}
/*******************************************************************************
** Getter for conditionalFilterList
*******************************************************************************/
public List<AbstractConditionalFilter> getConditionalFilterList()
{
return (this.conditionalFilterList);
}
/*******************************************************************************
** Setter for conditionalFilterList
*******************************************************************************/
public void setConditionalFilterList(List<AbstractConditionalFilter> conditionalFilterList)
{
this.conditionalFilterList = conditionalFilterList;
}
/*******************************************************************************
** Fluent setter for conditionalFilterList
*******************************************************************************/
public AbstractWidgetValueSourceWithFilter withConditionalFilterList(List<AbstractConditionalFilter> conditionalFilterList)
{
this.conditionalFilterList = conditionalFilterList;
return (this);
}
}

View File

@ -0,0 +1,162 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
/*******************************************************************************
**
*******************************************************************************/
public class HtmlWrapper implements Serializable
{
private String prefix;
private String suffix;
public static final HtmlWrapper SUBHEADER = new HtmlWrapper("<h4>", "</h4>");
public static final HtmlWrapper BIG_CENTERED = new HtmlWrapper("<div style='font-size: 2rem; font-weight: 400; line-height: 1.625; text-align: center; padding-bottom: 8px;'>", "</div>");
public static final HtmlWrapper INDENT_1 = new HtmlWrapper("<div style='padding-left: 1rem;'>", "</div>");
public static final HtmlWrapper INDENT_2 = new HtmlWrapper("<div style='padding-left: 2rem;'>", "</div>");
public static final HtmlWrapper FLOAT_RIGHT = new HtmlWrapper("<div style='float: right'>", "</div>");
public static final HtmlWrapper RULE_ABOVE = new HtmlWrapper("""
<hr style="opacity: 0.25; height: 0.0625rem; border-width: 0; margin-bottom: 1rem; background-image: linear-gradient(to right, rgba(52, 71, 103, 0), rgba(52, 71, 103, 0.4), rgba(52, 71, 103, 0));" />
""", "");
public static final String STYLE_BIG_CENTERED = "font-size: 2rem; font-weight: 400; line-height: 1.625; text-align: center; padding-bottom: 8px; ";
public static final String STYLE_MEDIUM_CENTERED = "font-size: 1.5rem; font-weight: 400; line-height: 1.625; text-align: center; padding-bottom: 4px; ";
public static final String STYLE_INDENT_1 = "padding-left: 1rem; ";
public static final String STYLE_INDENT_2 = "padding-left: 2rem; ";
public static final String STYLE_FLOAT_RIGHT = "float: right; ";
public static final String STYLE_RED = "color: red; ";
/*******************************************************************************
**
*******************************************************************************/
public HtmlWrapper(String prefix, String suffix)
{
this.prefix = prefix;
this.suffix = suffix;
}
/*******************************************************************************
**
*******************************************************************************/
public static HtmlWrapper divWithStyles(String... styles)
{
String style = StringUtils.join("", Arrays.asList(styles));
return (new HtmlWrapper("<div style=\"" + style + "\">", "</div>"));
}
/*******************************************************************************
**
*******************************************************************************/
public static HtmlWrapper width(String amount)
{
return (new HtmlWrapper("<div style='width: " + amount + "'>", "</div>"));
}
/*******************************************************************************
**
*******************************************************************************/
public static HtmlWrapper paddingTop(String amount)
{
return (new HtmlWrapper("<div style='padding-top: " + amount + "'>", "</div>"));
}
/*******************************************************************************
**
*******************************************************************************/
public static String styleWidth(String amount)
{
return ("width: " + amount);
}
/*******************************************************************************
**
*******************************************************************************/
public String wrap(String content)
{
return (Objects.requireNonNullElse(prefix, "")
+ "\n" + Objects.requireNonNullElse(content, "") + "\n"
+ Objects.requireNonNullElse(suffix, "") + "\n");
}
/*******************************************************************************
** Getter for prefix
**
*******************************************************************************/
public String getPrefix()
{
return prefix;
}
/*******************************************************************************
** Setter for prefix
**
*******************************************************************************/
public void setPrefix(String prefix)
{
this.prefix = prefix;
}
/*******************************************************************************
** Getter for suffix
**
*******************************************************************************/
public String getSuffix()
{
return suffix;
}
/*******************************************************************************
** Setter for suffix
**
*******************************************************************************/
public void setSuffix(String suffix)
{
this.suffix = suffix;
}
}

View File

@ -0,0 +1,102 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.io.Serializable;
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.widgets.RenderWidgetInput;
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 com.kingsrook.qqq.backend.core.utils.collections.MutableList;
/*******************************************************************************
**
*******************************************************************************/
public class IfInputVariableExistsFilter extends AbstractConditionalFilter
{
private String inputVariableName;
private QQueryFilter filter;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public IfInputVariableExistsFilter()
{
}
/*******************************************************************************
**
*******************************************************************************/
public IfInputVariableExistsFilter(String inputVariableName, QQueryFilter filter)
{
this.inputVariableName = inputVariableName;
this.filter = filter;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public boolean testCondition(RenderWidgetInput renderWidgetInput)
{
return (StringUtils.hasContent(ValueUtils.getValueAsString(renderWidgetInput.getQueryParams().get(inputVariableName))));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QQueryFilter getFilter(RenderWidgetInput renderWidgetInput)
{
QQueryFilter returnFilter = filter.clone();
for(QFilterCriteria criterion : CollectionUtils.nonNullList(returnFilter.getCriteria()))
{
if(criterion.getValues() != null)
{
criterion.setValues(new MutableList<>(criterion.getValues()));
for(int i = 0; i < criterion.getValues().size(); i++)
{
Serializable value = criterion.getValues().get(i);
if(value instanceof String valueString && valueString.equals("${input." + inputVariableName + "}"))
{
criterion.getValues().set(i, renderWidgetInput.getQueryParams().get(inputVariableName));
}
}
}
}
return (returnFilter);
}
}

View File

@ -0,0 +1,130 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class QNoCodeWidgetMetaData extends QWidgetMetaData
{
private List<AbstractWidgetValueSource> values = new ArrayList<>();
private List<AbstractWidgetOutput> outputs = new ArrayList<>();
/*******************************************************************************
** Getter for values
*******************************************************************************/
public List<AbstractWidgetValueSource> getValues()
{
return (this.values);
}
/*******************************************************************************
** Setter for values
*******************************************************************************/
public void setValues(List<AbstractWidgetValueSource> values)
{
this.values = values;
}
/*******************************************************************************
** Fluent setter to add a single value
*******************************************************************************/
public QNoCodeWidgetMetaData withValue(AbstractWidgetValueSource value)
{
if(this.values == null)
{
this.values = new ArrayList<>();
}
this.values.add(value);
return (this);
}
/*******************************************************************************
** Fluent setter for values
*******************************************************************************/
public QNoCodeWidgetMetaData withValues(List<AbstractWidgetValueSource> values)
{
this.values = values;
return (this);
}
/*******************************************************************************
** Getter for outputs
*******************************************************************************/
public List<AbstractWidgetOutput> getOutputs()
{
return (this.outputs);
}
/*******************************************************************************
** Setter for outputs
*******************************************************************************/
public void setOutputs(List<AbstractWidgetOutput> outputs)
{
this.outputs = outputs;
}
/*******************************************************************************
** Fluent setter to add a single output
*******************************************************************************/
public QNoCodeWidgetMetaData withOutput(AbstractWidgetOutput output)
{
if(this.outputs == null)
{
this.outputs = new ArrayList<>();
}
this.outputs.add(output);
return (this);
}
/*******************************************************************************
** Fluent setter for outputs
*******************************************************************************/
public QNoCodeWidgetMetaData withOutputs(List<AbstractWidgetOutput> outputs)
{
this.outputs = outputs;
return (this);
}
}

View File

@ -0,0 +1,100 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.dashboard.nocode;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.queues.GetQueueSize;
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.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
/*******************************************************************************
**
*******************************************************************************/
public class QueueSizeWidgetValue extends AbstractWidgetValueSource
{
private static final QLogger LOG = QLogger.getLogger(QueueSizeWidgetValue.class);
private String queueName;
/*******************************************************************************
**
*******************************************************************************/
@Override
public Object evaluate(Map<String, Object> context, RenderWidgetInput input) throws QException
{
QQueueMetaData queue = QContext.getQInstance().getQueue(queueName);
QQueueProviderMetaData queueProvider = QContext.getQInstance().getQueueProvider(queue.getProviderName());
return (new GetQueueSize().getQueueSize(queueProvider, queue));
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public QueueSizeWidgetValue withName(String name)
{
setName(name);
return (this);
}
/*******************************************************************************
** Getter for queueName
*******************************************************************************/
public String getQueueName()
{
return (this.queueName);
}
/*******************************************************************************
** Setter for queueName
*******************************************************************************/
public void setQueueName(String queueName)
{
this.queueName = queueName;
}
/*******************************************************************************
** Fluent setter for queueName
*******************************************************************************/
public QueueSizeWidgetValue withQueueName(String queueName)
{
this.queueName = queueName;
return (this);
}
}

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