Compare commits

...

282 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
a7faf52817 Merge branch 'release/0.11.0' 2023-01-30 13:37:55 -06:00
45c703a6df Update for next development version 2023-01-30 13:31:20 -06:00
5a9a0754a6 Update versions for release 2023-01-30 13:31:17 -06:00
4790f55243 Fixing scheduled process context; better thread names; add serverInfo endpoint 2023-01-26 21:55:24 -06:00
f0450ef621 PRDONE-170 - Adding support for passing in custom ScriptUtls for scripts. 2023-01-26 12:45:07 -06:00
ed5839aa0a Add /run endpoint for running processes w/o any frontend 2023-01-26 11:23:41 -06:00
56f05c74fc Add secrets manager; update org.json 2023-01-26 11:15:47 -06:00
4f8f8bad9a Initial checkin 2023-01-26 11:15:02 -06:00
4a1853faa5 SPRINT-19: added more chart data and widget meta data 2023-01-25 19:13:35 -06:00
c972f9cecc Update to allow _qStepTimeoutMillis to come from formBody 2023-01-25 10:09:45 -06:00
efa796bb39 Add property/env to add processTag to all logs 2023-01-24 15:56:50 -06:00
a05e74a7d0 SPRINT-19: added stacked bar chart, more widget metadata 2023-01-24 15:52:15 -06:00
41fcb09c70 Update to reset memory record store before and after each 2023-01-24 14:22:09 -06:00
6b12390f6c make scheduler look at env var in addition to system property for deciding whether to start or not. 2023-01-24 14:19:55 -06:00
9889320fa6 add call to validate api responses to doInsert (POST) 2023-01-24 14:08:45 -06:00
c00120a1fc add overloads that take 'String message, LogPair... logPairs' 2023-01-24 13:52:44 -06:00
029f436071 Update to cache script and revision ids (could be better); new AccumulatingBuildScriptLogAndScriptLogLineExecutionLogger, to just do 2 inserts across multiple script runs; add child-log-lines to script log table 2023-01-24 11:57:50 -06:00
f1ff53059f Handle null case in convertInputIdsToEnumIdType 2023-01-24 11:08:44 -06:00
18a8656ba2 Fix enum looks by ids of wrong type 2023-01-24 11:04:34 -06:00
e75b645639 Turning up logging 2023-01-20 15:14:10 -06:00
804efbd608 SPRINT-19: fixed xbar images 2023-01-20 13:13:29 -06:00
fe30d2654b update to implicity add a queryJoin, if a filter field calls out a table name that matches a join in the instance 2023-01-20 10:05:44 -06:00
97b803a86a add audit joins, move names to constants; fixes 2023-01-20 10:03:52 -06:00
2daa42aeb1 Merge branch 'feature/sprint-19' of github.com:Kingsrook/qqq into feature/sprint-19 2023-01-19 16:32:58 -06:00
4344fa472a Initial checkin 2023-01-19 16:31:18 -06:00
6bb810c1f4 Good exception for bad table name 2023-01-19 16:30:59 -06:00
d65af53090 Add some withers 2023-01-19 16:30:44 -06:00
ac6a7ba15a fix setValueIfTableHasField - should catch if field isn't found (helps tables w/o createDate work) 2023-01-19 16:12:30 -06:00
1c150e207a add nonNullArray and mergeLists 2023-01-19 16:12:02 -06:00
f9408716ac update to propagate default values into state before frontend steps 2023-01-19 16:11:45 -06:00
396f02265b explicit errors if context isn't setup 2023-01-19 16:11:19 -06:00
a1674792c6 Merge branch 'dev' into feature/sprint-19 2023-01-18 17:36:46 -06:00
22565b3ecd Initial checkin 2023-01-18 14:19:12 -06:00
d2e7b794f4 moving QLogger package 2023-01-18 12:11:40 -06:00
7000da409a moving standard lambdas to common package 2023-01-18 12:11:40 -06:00
3f0e09e32a Adding javalin access logs 2023-01-18 12:11:40 -06:00
70d9d259c1 Adding heavy field concept 2023-01-18 12:11:35 -06:00
4dc9c52ee0 Adding toLogPair method 2023-01-18 11:56:10 -06:00
964405d210 Initial checkin 2023-01-18 11:55:17 -06:00
369ba3c8d7 switch syslog to a json format (via patternLayout) 2023-01-18 11:46:12 -06:00
24d5406ee3 add filterStackTrace; move unsafeSupplier 2023-01-18 11:40:39 -06:00
984012f3a3 heavy fields, qvalueFormatter static 2023-01-18 11:39:47 -06:00
e8264d915f hotfix: updated to move pie chart label to be specified in the metacritic, added 'add' to pom resolver 2023-01-18 11:33:10 -06:00
46adfa8e24 WIP - logger migrations; initial work for data bag view widget 2023-01-17 10:44:45 -06:00
178078282c Switch to use QLogger everywhere 2023-01-17 10:44:45 -06:00
d03e947889 Merge branch 'dev' into feature/sprint-19 2023-01-17 10:29:16 -06:00
43de1cf749 Add post to download endpoint 2023-01-16 09:03:13 -06:00
d3fa1df56f Implementation of QContext everywhere, instead of passing QInstance and QSession in all ActionInputs 2023-01-15 19:41:23 -06:00
69a6104393 updated snapshot version, improved end of sprint script 2023-01-13 15:59:01 -06:00
e831c75e1e Merge branch 'dev' of github.com:Kingsrook/qqq into dev 2023-01-13 15:56:56 -06:00
eba9b0b4af Updating to <revision>0.11.0</revision> 2023-01-13 15:39:36 -06:00
9f82f35bcf Merge tag 'version-0.10.0' into dev
Tag release
2023-01-13 15:39:32 -06:00
680696491f Update for next development version 2023-01-13 15:36:06 -06:00
520 changed files with 45474 additions and 3232 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.10.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

@ -56,20 +56,31 @@
<groupId>software.amazon.awssdk</groupId>
<artifactId>quicksight</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-secretsmanager</artifactId>
<version>1.12.385</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.0-rc1</version>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.13.0</version>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
<version>20230227</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.actions;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
@ -50,11 +52,13 @@ public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O
*******************************************************************************/
public Future<Void> executeAsync(I input, O output)
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<Void> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() ->
{
try
{
QContext.init(capturedContext);
execute(input, output);
completableFuture.complete(null);
}
@ -62,6 +66,10 @@ public abstract class AbstractQActionBiConsumer<I extends AbstractActionInput, O
{
completableFuture.completeExceptionally(e);
}
finally
{
QContext.clear();
}
});
return (completableFuture);
}

View File

@ -25,6 +25,8 @@ package com.kingsrook.qqq.backend.core.actions;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
@ -50,11 +52,13 @@ public abstract class AbstractQActionFunction<I extends AbstractActionInput, O e
*******************************************************************************/
public Future<O> executeAsync(I input)
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<O> completableFuture = new CompletableFuture<>();
Executors.newCachedThreadPool().submit(() ->
{
try
{
QContext.init(capturedContext);
O output = execute(input);
completableFuture.complete(output);
}
@ -62,6 +66,10 @@ public abstract class AbstractQActionFunction<I extends AbstractActionInput, O e
{
completableFuture.completeExceptionally(e);
}
finally
{
QContext.clear();
}
});
return (completableFuture);
}

View File

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

View File

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

@ -30,13 +30,15 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import com.kingsrook.qqq.backend.core.context.CapturedContext;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -44,7 +46,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class AsyncJobManager
{
private static final Logger LOG = LogManager.getLogger(AsyncJobManager.class);
private static final QLogger LOG = QLogger.getLogger(AsyncJobManager.class);
@ -72,8 +74,10 @@ public class AsyncJobManager
try
{
CapturedContext capturedContext = QContext.capture();
CompletableFuture<T> future = CompletableFuture.supplyAsync(() ->
{
QContext.init(capturedContext);
return (runAsyncJob(jobName, asyncJob, uuidAndTypeStateKey, asyncJobStatus));
});
@ -147,12 +151,13 @@ 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
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}

View File

@ -27,9 +27,10 @@ import java.util.Optional;
import java.util.concurrent.TimeUnit;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeFunction;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
/*******************************************************************************
@ -40,13 +41,14 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class AsyncRecordPipeLoop
{
private static final Logger LOG = LogManager.getLogger(AsyncRecordPipeLoop.class);
private static final QLogger LOG = QLogger.getLogger(AsyncRecordPipeLoop.class);
private static final int TIMEOUT_AFTER_NO_RECORDS_MS = 10 * 60 * 1000;
private static final int MAX_SLEEP_MS = 1000;
private static final int INIT_SLEEP_MS = 10;
private static final int MIN_RECORDS_TO_CONSUME = 10;
private static final int MAX_SLEEP_MS = 1000;
private static final int INIT_SLEEP_MS = 10;
private Integer minRecordsToConsume = 10;
@ -62,7 +64,7 @@ public class AsyncRecordPipeLoop
** @param consumer lambda that consumes records from the pipe
* e.g., a transform/load step.
*******************************************************************************/
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable> supplier, UnsafeSupplier<Integer> consumer) throws QException
public int run(String jobName, Integer recordLimit, RecordPipe recordPipe, UnsafeFunction<AsyncJobCallback, ? extends Serializable, QException> supplier, UnsafeSupplier<Integer, QException> consumer) throws QException
{
///////////////////////////////////////////////////
// start the extraction function as an async job //
@ -82,7 +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. //
@ -178,29 +180,32 @@ public class AsyncRecordPipeLoop
/*******************************************************************************
**
** Getter for minRecordsToConsume
*******************************************************************************/
@FunctionalInterface
public interface UnsafeFunction<T, R>
public Integer getMinRecordsToConsume()
{
/*******************************************************************************
**
*******************************************************************************/
R apply(T t) throws QException;
return (this.minRecordsToConsume);
}
/*******************************************************************************
**
** Setter for minRecordsToConsume
*******************************************************************************/
@FunctionalInterface
public interface UnsafeSupplier<T>
public void setMinRecordsToConsume(Integer minRecordsToConsume)
{
/*******************************************************************************
**
*******************************************************************************/
T get() throws QException;
this.minRecordsToConsume = minRecordsToConsume;
}
/*******************************************************************************
** Fluent setter for minRecordsToConsume
*******************************************************************************/
public AsyncRecordPipeLoop withMinRecordsToConsume(Integer minRecordsToConsume)
{
this.minRecordsToConsume = minRecordsToConsume;
return (this);
}
}

View File

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

View File

@ -0,0 +1,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,9 +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;
@ -37,7 +46,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAuto
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.QLogger;
import org.apache.commons.lang.NotImplementedException;
@ -76,7 +84,7 @@ public class RecordAutomationStatusUpdater
String className = stackTraceElement.getClassName();
if(className.contains("com.kingsrook.qqq.backend.core.actions.automation") && !className.equals(RecordAutomationStatusUpdater.class.getName()) && !className.endsWith("Test"))
{
LOG.debug(session, "Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
LOG.debug("Avoiding re-setting automation status to PENDING_UPDATE while running an automation");
return (false);
}
}
@ -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);
}
}
@ -143,8 +200,7 @@ public class RecordAutomationStatusUpdater
boolean didSetStatusField = setAutomationStatusInRecords(session, table, records, automationStatus);
if(didSetStatusField)
{
UpdateInput updateInput = new UpdateInput(instance);
updateInput.setSession(session);
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(table.getName());
/////////////////////////////////////////////////////////////////////////////////////
@ -156,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,21 +25,24 @@ package com.kingsrook.qqq.backend.core.actions.automation.polling;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.async.AsyncRecordPipeLoop;
import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater;
import com.kingsrook.qqq.backend.core.actions.automation.RunRecordScriptAutomationHandler;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
@ -47,21 +50,22 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.automation.RecordAutomationInput;
import com.kingsrook.qqq.backend.core.model.automation.TableTrigger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTrackingType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEvent;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.StandardScheduledExecutor;
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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -75,7 +79,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class PollingAutomationPerTableRunner implements Runnable
{
private static final Logger LOG = LogManager.getLogger(PollingAutomationPerTableRunner.class);
private static final QLogger LOG = QLogger.getLogger(PollingAutomationPerTableRunner.class);
private final TableActions tableActions;
private final String name;
@ -88,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
@ -103,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));
}
}
@ -175,14 +159,16 @@ public class PollingAutomationPerTableRunner implements Runnable
@Override
public void run()
{
QContext.init(instance, sessionSupplier.get());
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName(name + StandardScheduledExecutor.newThreadNameRandomSuffix());
Thread.currentThread().setName(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)
{
@ -191,6 +177,7 @@ public class PollingAutomationPerTableRunner implements Runnable
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}
@ -199,8 +186,12 @@ public class PollingAutomationPerTableRunner implements Runnable
/*******************************************************************************
** Query for and process records that have a PENDING_INSERT or PENDING_UPDATE status on a given table.
*******************************************************************************/
private void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus, List<TableAutomationAction> actions) throws QException
public void processTableInsertOrUpdate(QTableMetaData table, QSession session, AutomationStatus automationStatus) throws QException
{
/////////////////////////////////////////////////////////////////////////
// get the actions to run against this table in this automation status //
/////////////////////////////////////////////////////////////////////////
List<TableAutomationAction> actions = getTableActions(table, automationStatus);
if(CollectionUtils.nullSafeIsEmpty(actions))
{
return;
@ -219,8 +210,7 @@ public class PollingAutomationPerTableRunner implements Runnable
asyncRecordPipeLoop.run("PollingAutomationRunner>Query>" + automationStatus + ">" + table.getName(), null, recordPipe, (status) ->
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
AutomationStatusTrackingType statusTrackingType = automationDetails.getStatusTracking().getType();
@ -246,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
@ -274,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);
List<QRecord> matchingQRecords = getRecordsMatchingActionFilter(table, records, action);
LOG.debug("Of the {} records that were pending automations, {} of them match the filter on the action {}", records.size(), matchingQRecords.size(), action);
if(CollectionUtils.nullSafeHasContents(matchingQRecords))
{
LOG.debug(" Processing " + matchingQRecords.size() + " records in " + table + " for action " + action);
applyActionToMatchingRecords(instance, session, table, matchingQRecords, action);
applyActionToMatchingRecords(table, matchingQRecords, action);
}
}
catch(Exception e)
@ -316,10 +359,9 @@ public class PollingAutomationPerTableRunner implements Runnable
** but that will almost certainly give potentially different results than a true
** backend - e.g., just consider if the DB is case-sensitive for strings...
*******************************************************************************/
private List<QRecord> getRecordsMatchingActionFilter(QSession session, QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
private List<QRecord> getRecordsMatchingActionFilter(QTableMetaData table, List<QRecord> records, TableAutomationAction action) throws QException
{
QueryInput queryInput = new QueryInput(instance);
queryInput.setSession(session);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
QQueryFilter filter = new QQueryFilter();
@ -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()))
{
@ -368,8 +410,7 @@ public class PollingAutomationPerTableRunner implements Runnable
// tell it to SKIP frontend steps. //
// give the process a callback w/ a query filter that has the p-keys of these records. //
/////////////////////////////////////////////////////////////////////////////////////////
RunProcessInput runProcessInput = new RunProcessInput(instance);
runProcessInput.setSession(session);
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(action.getProcessName());
runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
runProcessInput.setCallback(new QProcessCallback()
@ -382,20 +423,29 @@ public class PollingAutomationPerTableRunner implements Runnable
}
});
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
if(runProcessOutput.getException().isPresent())
try
{
throw (runProcessOutput.getException().get());
QContext.pushAction(runProcessInput);
RunProcessAction runProcessAction = new RunProcessAction();
RunProcessOutput runProcessOutput = runProcessAction.execute(runProcessInput);
if(runProcessOutput.getException().isPresent())
{
throw (runProcessOutput.getException().get());
}
}
finally
{
QContext.popAction();
}
}
else if(action.getCodeReference() != null)
{
LOG.debug(" Executing action: [" + action.getName() + "] as code reference: " + action.getCodeReference());
RecordAutomationInput input = new RecordAutomationInput(instance);
input.setSession(session);
RecordAutomationInput input = new RecordAutomationInput();
input.setTableName(table.getName());
input.setRecordList(records);
input.setAction(action);
RecordAutomationHandler recordAutomationHandler = QCodeLoader.getRecordAutomationHandler(action);
recordAutomationHandler.execute(input);

View File

@ -113,8 +113,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
/////////////////////////
// insert the children //
/////////////////////////
InsertInput insertInput = new InsertInput(getInsertInput().getInstance());
insertInput.setSession(getInsertInput().getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName(getChildTableName());
insertInput.setRecords(childrenToInsert);
insertInput.setTransaction(this.insertInput.getTransaction());
@ -145,8 +144,7 @@ public abstract class ChildInserterPostInsertCustomizer extends AbstractPostInse
////////////////////////////////////////////////////////////////////////////
// update the originally inserted records to reference their new children //
////////////////////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput(insertInput.getInstance());
updateInput.setSession(getInsertInput().getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInsertInput().getTableName());
updateInput.setRecords(recordsToUpdate);
updateInput.setTransaction(this.insertInput.getTransaction());

View File

@ -28,13 +28,13 @@ import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationHandler
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -42,7 +42,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QCodeLoader
{
private static final Logger LOG = LogManager.getLogger(QCodeLoader.class);
private static final QLogger LOG = QLogger.getLogger(QCodeLoader.class);
@ -102,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 //
@ -141,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 //
@ -180,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;
@ -89,8 +88,7 @@ public abstract class AbstractWidgetRenderer
pvsLabels.add(pvsLabel);
pvsNames.add(possibleValueSourceName);
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput(input.getInstance());
pvsInput.setSession(input.getSession());
SearchPossibleValueSourceInput pvsInput = new SearchPossibleValueSourceInput();
pvsInput.setPossibleValueSourceName(possibleValueSourceName);
if(dropdownData.getForeignKeyFieldName() != null)
@ -123,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;
@ -70,6 +71,7 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
{
return (new Builder(new QWidgetMetaData()
.withName(join.getName())
.withIsCard(true)
.withCodeReference(new QCodeReference(ChildRecordListRenderer.class, null))
.withType(WidgetType.CHILD_RECORD_LIST.getType())
.withDefaultValue("joinName", join.getName())));
@ -116,6 +118,17 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
/*******************************************************************************
**
*******************************************************************************/
public Builder withMaxRows(Integer maxRows)
{
widgetMetaData.withDefaultValue("maxRows", maxRows);
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
@ -150,12 +163,17 @@ 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 //
////////////////////////////////////////////////////////
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName(join.getLeftTable());
getInput.setPrimaryKey(id);
GetOutput getOutput = new GetAction().execute(getInput);
@ -177,16 +195,16 @@ public class ChildRecordListRenderer extends AbstractWidgetRenderer
}
filter.setOrderBys(join.getOrderBys());
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(join.getRightTable());
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,92 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.dashboard.widgets;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput;
import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput;
import com.kingsrook.qqq.backend.core.model.dashboard.widgets.QWidgetData;
/*******************************************************************************
**
*******************************************************************************/
public class DefaultWidgetRenderer extends AbstractWidgetRenderer
{
/*******************************************************************************
**
*******************************************************************************/
@Override
public RenderWidgetOutput render(RenderWidgetInput input) throws QException
{
return new RenderWidgetOutput(new DefaultWidgetData(input));
}
/*******************************************************************************
**
*******************************************************************************/
public static class DefaultWidgetData extends QWidgetData
{
private final String type;
private final Map<String, String> queryParams;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public DefaultWidgetData(RenderWidgetInput renderWidgetInput)
{
this.type = renderWidgetInput.getWidgetMetaData().getType();
this.queryParams = renderWidgetInput.getQueryParams();
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String getType()
{
return (type);
}
/*******************************************************************************
** Getter for queryParams
**
*******************************************************************************/
public Map<String, String> getQueryParams()
{
return queryParams;
}
}
}

View File

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

@ -23,11 +23,10 @@ package com.kingsrook.qqq.backend.core.actions.permissions;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -35,7 +34,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class BulkTableActionProcessPermissionChecker implements CustomPermissionChecker
{
private static final Logger LOG = LogManager.getLogger(BulkTableActionProcessPermissionChecker.class);
private static final QLogger LOG = QLogger.getLogger(BulkTableActionProcessPermissionChecker.class);
@ -52,8 +51,7 @@ public class BulkTableActionProcessPermissionChecker implements CustomPermission
String tableName = parts[0];
String bulkActionName = parts[1];
AbstractTableActionInput tableActionInput = new AbstractTableActionInput(actionInput.getInstance());
tableActionInput.setSession(actionInput.getSession());
AbstractTableActionInput tableActionInput = new AbstractTableActionInput();
tableActionInput.setTableName(tableName);
switch(bulkActionName)

View File

@ -29,7 +29,9 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -44,8 +46,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -53,7 +53,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class PermissionsHelper
{
private static final Logger LOG = LogManager.getLogger(PermissionsHelper.class);
private static final QLogger LOG = QLogger.getLogger(PermissionsHelper.class);
@ -73,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));
}
@ -103,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())
@ -168,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)
{
@ -181,7 +194,7 @@ public class PermissionsHelper
return;
}
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName(), actionInput);
commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName());
}
@ -209,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());
}
@ -238,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());
}
@ -267,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());
}
@ -387,6 +400,11 @@ public class PermissionsHelper
*******************************************************************************/
public static QPermissionRules getEffectivePermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules, QInstance instance)
{
if(metaDataWithPermissionRules.getPermissionRules() == null)
{
LOG.warn("Null permission rules on meta data object [" + metaDataWithPermissionRules.getClass().getSimpleName() + "][" + metaDataWithPermissionRules.getName() + "] - does the instance need enriched? Returning instance default rules.");
return (instance.getDefaultPermissionRules());
}
return (metaDataWithPermissionRules.getPermissionRules());
}
@ -431,7 +449,7 @@ public class PermissionsHelper
}
}
if(hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType))
if(hasPermission(QContext.getQSession(), permissionBaseName, effectivePermissionSubType))
{
return (PermissionCheckResult.ALLOW);
}
@ -519,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);
@ -529,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 com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
@ -42,8 +43,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMet
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -52,7 +51,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RunBackendStepAction
{
private static final Logger LOG = LogManager.getLogger(RunBackendStepAction.class);
private static final QLogger LOG = QLogger.getLogger(RunBackendStepAction.class);
@ -107,7 +106,7 @@ public class RunBackendStepAction
return;
}
List<QFieldMetaData> fieldsToGet = new ArrayList<>();
List<QFieldMetaData> fieldsToGet = new ArrayList<>();
List<QFieldMetaData> requiredFieldsMissing = new ArrayList<>();
for(QFieldMetaData field : inputMetaData.getFieldList())
{
@ -175,8 +174,7 @@ public class RunBackendStepAction
{
if(CollectionUtils.nullSafeIsEmpty(runBackendStepInput.getRecords()))
{
QueryInput queryInput = new QueryInput(runBackendStepInput.getInstance());
queryInput.setSession(runBackendStepInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(inputMetaData.getRecordListMetaData().getTableName());
// todo - handle this being async (e.g., http)

View File

@ -31,10 +31,12 @@ import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.dashboard.widgets.NoCodeWidgetRenderer;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
@ -48,7 +50,10 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.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;
@ -58,10 +63,9 @@ import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import com.kingsrook.qqq.backend.core.state.StateType;
import com.kingsrook.qqq.backend.core.state.UUIDAndTypeStateKey;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -70,7 +74,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RunProcessAction
{
private static final Logger LOG = LogManager.getLogger(RunProcessAction.class);
private static final QLogger LOG = QLogger.getLogger(RunProcessAction.class);
public static final String BASEPULL_THIS_RUNTIME_KEY = "basepullThisRuntimeKey";
public static final String BASEPULL_LAST_RUNTIME_KEY = "basepullLastRuntimeKey";
@ -143,7 +147,7 @@ public class RunProcessAction
QStepMetaData step = stepList.get(0);
lastStepName = step.getName();
if(step instanceof QFrontendStepMetaData)
if(step instanceof QFrontendStepMetaData frontendStep)
{
////////////////////////////////////////////////////////////////
// Handle what to do with frontend steps, per request setting //
@ -153,6 +157,8 @@ public class RunProcessAction
case BREAK ->
{
LOG.trace("Breaking process [" + process.getName() + "] at frontend step (as requested by caller): " + step.getName());
processFrontendStepFieldDefaultValues(processState, frontendStep);
processFrontendComponents(processState, frontendStep);
processState.setNextStepName(step.getName());
break STEP_LOOP;
}
@ -230,6 +236,42 @@ public class RunProcessAction
/*******************************************************************************
**
*******************************************************************************/
private void processFrontendComponents(ProcessState processState, QFrontendStepMetaData frontendStep) throws QException
{
for(QFrontendComponentMetaData component : CollectionUtils.nonNullList(frontendStep.getComponents()))
{
if(component instanceof NoCodeWidgetFrontendComponentMetaData noCodeWidgetComponent)
{
NoCodeWidgetRenderer noCodeWidgetRenderer = new NoCodeWidgetRenderer();
Map<String, Object> context = noCodeWidgetRenderer.initContext(null);
context.putAll(processState.getValues());
String html = noCodeWidgetRenderer.renderOutputs(context, noCodeWidgetComponent.getOutputs());
processState.getValues().put(frontendStep.getName() + ".html", html);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private void processFrontendStepFieldDefaultValues(ProcessState processState, QFrontendStepMetaData step)
{
for(QFieldMetaData formField : CollectionUtils.mergeLists(step.getFormFields(), step.getInputFields(), step.getViewFields(), step.getOutputFields()))
{
if(formField.getDefaultValue() != null && processState.getValues().get(formField.getName()) == null)
{
processState.getValues().put(formField.getName(), formField.getDefaultValue());
}
}
}
/*******************************************************************************
** When we start running a process (or resuming it), get data in the RunProcessRequest
** either from the state provider (if they're found, for a resume).
@ -295,15 +337,25 @@ public class RunProcessAction
*******************************************************************************/
private void runBackendStep(RunProcessInput runProcessInput, QProcessMetaData process, RunProcessOutput runProcessOutput, UUIDAndTypeStateKey stateKey, QBackendStepMetaData backendStep, QProcessMetaData qProcessMetaData, ProcessState processState) throws Exception
{
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(runProcessInput.getInstance(), processState);
RunBackendStepInput runBackendStepInput = new RunBackendStepInput(processState);
runBackendStepInput.setProcessName(process.getName());
runBackendStepInput.setStepName(backendStep.getName());
runBackendStepInput.setTableName(process.getTableName());
runBackendStepInput.setSession(runProcessInput.getSession());
runBackendStepInput.setCallback(runProcessInput.getCallback());
runBackendStepInput.setFrontendStepBehavior(runProcessInput.getFrontendStepBehavior());
runBackendStepInput.setAsyncJobCallback(runProcessInput.getAsyncJobCallback());
runBackendStepInput.setTableName(process.getTableName());
if(!StringUtils.hasContent(runBackendStepInput.getTableName()))
{
////////////////////////////////////////////////////////////////
// help support generic (e.g., not tied-to-a-table) processes //
////////////////////////////////////////////////////////////////
if(runProcessInput.getValue("tableName") != null)
{
runBackendStepInput.setTableName(ValueUtils.getValueAsString(runProcessInput.getValue("tableName")));
}
}
///////////////////////////////////////////////////////////////
// if 'basepull' values are in the inputs, add to step input //
///////////////////////////////////////////////////////////////
@ -444,8 +496,7 @@ public class RunProcessAction
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
queryInput.setSession(runProcessInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()
@ -473,8 +524,7 @@ public class RunProcessAction
////////////
// update //
////////////
UpdateInput updateInput = new UpdateInput(runProcessInput.getInstance());
updateInput.setSession(runProcessInput.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(basepullTableName);
updateInput.setRecords(List.of(basepullRecord));
new UpdateAction().execute(updateInput);
@ -488,8 +538,7 @@ public class RunProcessAction
////////////////////////////////
// insert new basepull record //
////////////////////////////////
InsertInput insertInput = new InsertInput(runProcessInput.getInstance());
insertInput.setSession(runProcessInput.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName(basepullTableName);
insertInput.setRecords(List.of(basepullRecord));
new InsertAction().execute(insertInput);
@ -527,8 +576,7 @@ public class RunProcessAction
///////////////////////////////////////
// get the stored basepull timestamp //
///////////////////////////////////////
QueryInput queryInput = new QueryInput(runProcessInput.getInstance());
queryInput.setSession(runProcessInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(basepullTableName);
queryInput.setFilter(new QQueryFilter().withCriteria(
new QFilterCriteria()

View File

@ -0,0 +1,84 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.actions.queues;
import java.util.List;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSClientBuilder;
import com.amazonaws.services.sqs.model.GetQueueAttributesResult;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
**
*******************************************************************************/
public class GetQueueSize
{
private static final QLogger LOG = QLogger.getLogger(GetQueueSize.class);
/*******************************************************************************
**
*******************************************************************************/
public Integer getQueueSize(QQueueProviderMetaData queueProviderMetaData, QQueueMetaData queueMetaData) throws QException
{
try
{
//////////////////////////////////////////////////////////////////
// todo - handle other queue provider types, somewhere, somehow //
//////////////////////////////////////////////////////////////////
SQSQueueProviderMetaData queueProvider = (SQSQueueProviderMetaData) queueProviderMetaData;
BasicAWSCredentials credentials = new BasicAWSCredentials(queueProvider.getAccessKey(), queueProvider.getSecretKey());
final AmazonSQS sqs = AmazonSQSClientBuilder.standard()
.withRegion(queueProvider.getRegion())
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
String queueUrl = queueProvider.getBaseURL();
if(!queueUrl.endsWith("/"))
{
queueUrl += "/";
}
queueUrl += queueMetaData.getQueueName();
GetQueueAttributesResult queueAttributes = sqs.getQueueAttributes(queueUrl, List.of("ApproximateNumberOfMessages"));
String approximateNumberOfMessages = queueAttributes.getAttributes().get("ApproximateNumberOfMessages");
return (Integer.parseInt(approximateNumberOfMessages));
}
catch(Exception e)
{
LOG.warn("Error getting queue size", e, logPair("queueName", queueMetaData == null ? "null" : queueMetaData.getName()));
throw (new QException("Error getting queue size", e));
}
}
}

View File

@ -35,15 +35,14 @@ import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.scheduler.StandardScheduledExecutor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -51,7 +50,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class SQSQueuePoller implements Runnable
{
private static final Logger LOG = LogManager.getLogger(SQSQueuePoller.class);
private static final QLogger LOG = QLogger.getLogger(SQSQueuePoller.class);
///////////////////////////////////////////////
// todo - move these 2 to a "QBaseRunnable"? //
@ -70,8 +69,10 @@ public class SQSQueuePoller implements Runnable
@Override
public void run()
{
QContext.init(qInstance, sessionSupplier.get());
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName("SQSPoller>" + queueMetaData.getName() + StandardScheduledExecutor.newThreadNameRandomSuffix());
Thread.currentThread().setName("SQSPoller>" + queueMetaData.getName());
LOG.debug("Running " + this.getClass().getSimpleName() + "[" + queueMetaData.getName() + "]");
try
@ -125,12 +126,13 @@ public class SQSQueuePoller implements Runnable
/////////////////////////////////////////////////////////////////////////////////////
try
{
RunProcessInput runProcessInput = new RunProcessInput(qInstance);
runProcessInput.setSession(sessionSupplier.get());
RunProcessInput runProcessInput = new RunProcessInput();
runProcessInput.setProcessName(queueMetaData.getProcessName());
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)
@ -165,6 +171,7 @@ public class SQSQueuePoller implements Runnable
finally
{
Thread.currentThread().setName(originalThreadName);
QContext.clear();
}
}

View File

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

View File

@ -37,6 +37,7 @@ import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.ExcelStylerInterface;
import com.kingsrook.qqq.backend.core.actions.reporting.excelformatting.PlainExcelStyler;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
@ -45,8 +46,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dhatim.fastexcel.StyleSetter;
import org.dhatim.fastexcel.Workbook;
import org.dhatim.fastexcel.Worksheet;
@ -57,7 +56,7 @@ import org.dhatim.fastexcel.Worksheet;
*******************************************************************************/
public class ExcelExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(ExcelExportStreamer.class);
private static final QLogger LOG = QLogger.getLogger(ExcelExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
@ -50,8 +51,6 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -67,7 +66,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class ExportAction
{
private static final Logger LOG = LogManager.getLogger(ExportAction.class);
private static final QLogger LOG = QLogger.getLogger(ExportAction.class);
private boolean preExecuteRan = false;
private Integer countFromPreExecute = null;
@ -149,8 +148,7 @@ public class ExportAction
// set up a query input //
//////////////////////////
QueryAction queryAction = new QueryAction();
QueryInput queryInput = new QueryInput(exportInput.getInstance());
queryInput.setSession(exportInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(exportInput.getTableName());
queryInput.setFilter(exportInput.getQueryFilter());
queryInput.setLimit(exportInput.getLimit());
@ -350,8 +348,7 @@ public class ExportAction
if(exportInput.getLimit() == null || exportInput.getLimit() > reportFormat.getMaxRows())
{
CountInterface countInterface = backendModule.getCountInterface();
CountInput countInput = new CountInput(exportInput.getInstance());
countInput.setSession(exportInput.getSession());
CountInput countInput = new CountInput();
countInput.setTableName(exportInput.getTableName());
countInput.setFilter(exportInput.getQueryFilter());
CountOutput countOutput = countInterface.execute(countInput);

View File

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

View File

@ -45,13 +45,13 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QFormulaException;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.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;
@ -65,15 +65,14 @@ 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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -90,7 +89,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class GenerateReportAction
{
private static final Logger LOG = LogManager.getLogger(GenerateReportAction.class);
private static final QLogger LOG = QLogger.getLogger(GenerateReportAction.class);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// summaryAggregates and varianceAggregates are multi-level maps, ala: //
@ -216,8 +215,7 @@ public class GenerateReportAction
QMetaDataVariableInterpreter variableInterpreter = new QMetaDataVariableInterpreter();
variableInterpreter.addValueMap("input", reportInput.getInputValues());
ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession());
ExportInput exportInput = new ExportInput();
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(getTitle(reportView, variableInterpreter));
@ -227,7 +225,7 @@ public class GenerateReportAction
JoinsContext joinsContext = null;
if(StringUtils.hasContent(dataSource.getSourceTable()))
{
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins());
joinsContext = new JoinsContext(exportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), dataSource.getQueryFilter());
}
List<QFieldMetaData> fields = new ArrayList<>();
@ -277,8 +275,7 @@ public class GenerateReportAction
{
transformStep = QCodeLoader.getBackendStep(AbstractTransformStep.class, tableView.getRecordTransformStep());
transformStepInput = new RunBackendStepInput(reportInput.getInstance());
transformStepInput.setSession(reportInput.getSession());
transformStepInput = new RunBackendStepInput();
transformStepInput.setValues(reportInput.getInputValues());
transformStepOutput = new RunBackendStepOutput();
@ -304,15 +301,14 @@ public class GenerateReportAction
QQueryFilter queryFilter = dataSource.getQueryFilter() == null ? new QQueryFilter() : dataSource.getQueryFilter().clone();
setInputValuesInQueryFilter(reportInput, queryFilter);
QueryInput queryInput = new QueryInput(reportInput.getInstance());
queryInput.setSession(reportInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setRecordPipe(recordPipe);
queryInput.setTableName(dataSource.getSourceTable());
queryInput.setFilter(queryFilter);
queryInput.setQueryJoins(dataSource.getQueryJoins());
queryInput.setShouldTranslatePossibleValues(true);
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins())));
queryInput.setFieldsToTranslatePossibleValues(setupFieldsToTranslatePossibleValues(reportInput, dataSource, new JoinsContext(reportInput.getInstance(), dataSource.getSourceTable(), dataSource.getQueryJoins(), queryInput.getFilter())));
if(dataSource.getQueryInputCustomizer() != null)
{
@ -361,7 +357,7 @@ public class GenerateReportAction
////////////////////////////////////////////////
if(transformStep != null)
{
transformStep.postRun(transformStepInput, transformStepOutput);
transformStep.postRun(new BackendStepPostRunInput(transformStepInput), new BackendStepPostRunOutput(transformStepOutput));
}
}
@ -416,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());
}
@ -597,8 +577,7 @@ public class GenerateReportAction
QTableMetaData table = reportInput.getInstance().getTable(dataSource.getSourceTable());
SummaryOutput summaryOutput = computeSummaryRowsForView(reportInput, view, table);
ExportInput exportInput = new ExportInput(reportInput.getInstance());
exportInput.setSession(reportInput.getSession());
ExportInput exportInput = new ExportInput();
exportInput.setReportFormat(reportFormat);
exportInput.setFilename(reportInput.getFilename());
exportInput.setTitleRow(summaryOutput.titleRow);

View File

@ -30,14 +30,13 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QReportingException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.reporting.ExportInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -45,14 +44,14 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class JsonExportStreamer implements ExportStreamerInterface
{
private static final Logger LOG = LogManager.getLogger(JsonExportStreamer.class);
private static final QLogger LOG = QLogger.getLogger(JsonExportStreamer.class);
private ExportInput exportInput;
private QTableMetaData table;
private List<QFieldMetaData> fields;
private OutputStream outputStream;
private boolean needComma = false;
private boolean needComma = false;
private boolean prettyPrint = true;

View File

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

View File

@ -27,10 +27,9 @@ import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.SleepUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -39,7 +38,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class RecordPipe
{
private static final Logger LOG = LogManager.getLogger(RecordPipe.class);
private static final QLogger LOG = QLogger.getLogger(RecordPipe.class);
private static final long BLOCKING_SLEEP_MILLIS = 100;
private static final long MAX_SLEEP_LOOP_MILLIS = 300_000; // 5 minutes

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

@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.scripts;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
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.exceptions.QException;
@ -35,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptI
import com.kingsrook.qqq.backend.core.model.actions.scripts.RunAssociatedScriptOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
@ -46,6 +51,8 @@ import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
*******************************************************************************/
public class RunAssociatedScriptAction
{
private Map<AssociatedScriptCodeReference, ScriptRevision> scriptRevisionCache = new HashMap<>();
/*******************************************************************************
**
@ -54,32 +61,37 @@ public class RunAssociatedScriptAction
{
ActionHelper.validateSession(input);
Serializable scriptId = getScriptId(input);
if(scriptId == null)
{
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
}
ScriptRevision scriptRevision = getScriptRevision(input);
Script script = getScript(input, scriptId);
if(script.getCurrentScriptRevisionId() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] (scriptId=" + scriptId + ") does not have a current version."));
}
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
executeCodeInput.setSession(input.getSession());
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setInput(new HashMap<>(input.getInputValues()));
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.setCodeReference(new QCodeReference().withInlineCode(scriptRevision.getContents()).withCodeType(QCodeType.JAVA_SCRIPT)); // todo - code type as attribute of script!!
executeCodeInput.setExecutionLogger(new StoreScriptLogAndScriptLogLineExecutionLogger(scriptRevision.getScriptId(), scriptRevision.getId()));
/////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
@ -88,13 +100,42 @@ public class RunAssociatedScriptAction
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getScriptRevision(RunAssociatedScriptInput input) throws QException
{
if(!scriptRevisionCache.containsKey(input.getCodeReference()))
{
Serializable scriptId = getScriptId(input);
if(scriptId == null)
{
throw (new QNotFoundException("The input record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] does not have a script specified for [" + input.getCodeReference().getFieldName() + "]"));
}
Script script = getScript(input, scriptId);
if(script.getCurrentScriptRevisionId() == null)
{
throw (new QNotFoundException("The script for record [" + input.getCodeReference().getRecordTable() + "][" + input.getCodeReference().getRecordPrimaryKey()
+ "] (scriptId=" + scriptId + ") does not have a current version."));
}
ScriptRevision scriptRevision = getCurrentScriptRevision(input, script.getCurrentScriptRevisionId());
scriptRevisionCache.put(input.getCodeReference(), scriptRevision);
}
return scriptRevisionCache.get(input.getCodeReference());
}
/*******************************************************************************
**
*******************************************************************************/
private ScriptRevision getCurrentScriptRevision(RunAssociatedScriptInput input, Serializable scriptRevisionId) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("scriptRevision");
getInput.setPrimaryKey(scriptRevisionId);
GetOutput getOutput = new GetAction().execute(getInput);
@ -114,8 +155,7 @@ public class RunAssociatedScriptAction
*******************************************************************************/
private Script getScript(RunAssociatedScriptInput input, Serializable scriptId) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(scriptId);
GetOutput getOutput = new GetAction().execute(getInput);
@ -136,8 +176,7 @@ public class RunAssociatedScriptAction
*******************************************************************************/
private Serializable getScriptId(RunAssociatedScriptInput input) throws QException
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName(input.getCodeReference().getRecordTable());
getInput.setPrimaryKey(input.getCodeReference().getRecordPrimaryKey());
GetOutput getOutput = new GetAction().execute(getInput);

View File

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

@ -83,8 +83,7 @@ public class StoreAssociatedScriptAction
/////////////////////////////////////////////////////////////
QRecord associatedRecord;
{
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName(input.getTableName());
getInput.setPrimaryKey(input.getRecordPrimaryKey());
getInput.setShouldGenerateDisplayValues(true);
@ -107,8 +106,7 @@ public class StoreAssociatedScriptAction
////////////////////////////////////////////////////////////////////
// get the script type - that'll be part of the new script's name //
////////////////////////////////////////////////////////////////////
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("scriptType");
getInput.setPrimaryKey(associatedScript.getScriptTypeId());
getInput.setShouldGenerateDisplayValues(true);
@ -125,8 +123,7 @@ public class StoreAssociatedScriptAction
script = new QRecord();
script.setValue("scriptTypeId", associatedScript.getScriptTypeId());
script.setValue("name", associatedRecord.getRecordLabel() + " - " + scriptType.getRecordLabel());
InsertInput insertInput = new InsertInput(input.getInstance());
insertInput.setSession(input.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName("script");
insertInput.setRecords(List.of(script));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -135,8 +132,7 @@ public class StoreAssociatedScriptAction
/////////////////////////////////////////////////////////////
// update the associated record to point at the new script //
/////////////////////////////////////////////////////////////
UpdateInput updateInput = new UpdateInput(input.getInstance());
updateInput.setSession(input.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(input.getTableName());
updateInput.setRecords(List.of(new QRecord()
.withValue(table.getPrimaryKeyField(), associatedRecord.getValue(table.getPrimaryKeyField()))
@ -149,15 +145,13 @@ public class StoreAssociatedScriptAction
////////////////////////////////////////
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput(input.getInstance());
getInput.setSession(input.getSession());
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setPrimaryKey(existingScriptId);
GetOutput getOutput = new GetAction().execute(getInput);
script = getOutput.getRecord();
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName("scriptRevision");
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
@ -202,8 +196,7 @@ public class StoreAssociatedScriptAction
scriptRevision.setValue("author", "Unknown");
}
InsertInput insertInput = new InsertInput(input.getInstance());
insertInput.setSession(input.getSession());
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
insertInput.setRecords(List.of(scriptRevision));
InsertOutput insertOutput = new InsertAction().execute(insertInput);
@ -213,8 +206,7 @@ public class StoreAssociatedScriptAction
// update the script to point at the new revision //
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput(input.getInstance());
updateInput.setSession(input.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setRecords(List.of(script));
new UpdateAction().execute(updateInput);

View File

@ -80,8 +80,7 @@ public interface TestScriptActionInterface
*******************************************************************************/
default void execute(TestScriptInput input, TestScriptOutput output) throws QException
{
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput(input.getInstance());
executeCodeInput.setSession(input.getSession());
ExecuteCodeInput executeCodeInput = new ExecuteCodeInput();
executeCodeInput.setContext(new HashMap<>());
executeCodeInput.setCodeReference(input.getCodeReference());

View File

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

View File

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

View File

@ -24,12 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.scripts.logging;
import java.io.Serializable;
import java.util.UUID;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.scripts.ExecuteCodeInput;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -37,7 +36,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class Log4jCodeExecutionLogger implements QCodeExecutionLoggerInterface
{
private static final Logger LOG = LogManager.getLogger(Log4jCodeExecutionLogger.class);
private static final QLogger LOG = QLogger.getLogger(Log4jCodeExecutionLogger.class);
private QCodeReference qCodeReference;
private String uuid = UUID.randomUUID().toString();

View File

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

View File

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

View File

@ -24,19 +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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -45,7 +61,9 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class DeleteAction
{
private static final Logger LOG = LogManager.getLogger(DeleteAction.class);
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to delete";
@ -58,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)
{
@ -82,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,7 +295,8 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
QueryInput queryInput = new QueryInput(deleteInput.getInstance(), deleteInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTransaction(deleteInput.getTransaction());
queryInput.setTableName(deleteInput.getTableName());
queryInput.setFilter(deleteInput.getQueryFilter());
QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput);

View File

@ -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,10 +67,11 @@ 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;
private QValueFormatter qValueFormatter;
private QPossibleValueTranslator qPossibleValueTranslator;
@ -80,6 +84,11 @@ public class GetAction
ActionHelper.validateSession(getInput);
QTableMetaData table = getInput.getTable();
if(table == null)
{
throw (new QException("Requested to Get a record from an unrecognized table: " + getInput.getTableName()));
}
postGetRecordCustomizer = QCodeLoader.getTableCustomizer(AbstractPostQueryCustomizer.class, table, TableCustomizers.POST_QUERY_RECORD.getRole());
this.getInput = getInput;
@ -121,14 +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(getInput.getInstance());
insertInput.setSession(getInput.getSession());
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
@ -190,8 +219,7 @@ public class GetAction
QRecord recordToCache = mapSourceRecordToCacheRecord(table, recordFromSource);
recordToCache.setValue(table.getPrimaryKeyField(), cachedRecord.getValue(table.getPrimaryKeyField()));
UpdateInput updateInput = new UpdateInput(getInput.getInstance());
updateInput.setSession(getInput.getSession());
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(getInput.getTableName());
updateInput.setRecords(List.of(recordToCache));
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
@ -203,8 +231,7 @@ public class GetAction
/////////////////////////////////////////////////////////////////////////////
// if the record is no longer in the source, then remove it from the cache //
/////////////////////////////////////////////////////////////////////////////
DeleteInput deleteInput = new DeleteInput(getInput.getInstance());
deleteInput.setSession(getInput.getSession());
DeleteInput deleteInput = new DeleteInput();
deleteInput.setTableName(getInput.getTableName());
deleteInput.setPrimaryKeys(List.of(getOutput.getRecord().getValue(table.getPrimaryKeyField())));
new DeleteAction().execute(deleteInput);
@ -252,12 +279,13 @@ public class GetAction
/////////////////////////////////////////////////////
// do a Get on the source table, by the unique key //
/////////////////////////////////////////////////////
GetInput sourceGetInput = new GetInput(getInput.getInstance());
sourceGetInput.setSession(getInput.getSession());
GetInput sourceGetInput = new GetInput();
sourceGetInput.setTableName(sourceTableName);
sourceGetInput.setUniqueKey(getInput.getUniqueKey());
GetOutput sourceGetOutput = new GetAction().execute(sourceGetInput);
return (sourceGetOutput.getRecord());
QRecord outputRecord = sourceGetOutput.getRecord();
return (outputRecord);
}
@ -270,8 +298,7 @@ public class GetAction
@Override
public GetOutput execute(GetInput getInput) throws QException
{
QueryInput queryInput = new QueryInput(getInput.getInstance());
queryInput.setSession(getInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(getInput.getTableName());
//////////////////////////////////////////////////
@ -302,6 +329,9 @@ public class GetAction
}
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
@ -339,13 +369,13 @@ public class GetAction
if(getInput.getShouldGenerateDisplayValues())
{
if(qValueFormatter == null)
{
qValueFormatter = new QValueFormatter();
}
qValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
QValueFormatter.setDisplayValuesInRecords(getInput.getTable(), List.of(returnRecord));
}
//////////////////////////////////////////////////////////////////////////////
// note - shouldFetchHeavyFields should be handled by the underlying action //
//////////////////////////////////////////////////////////////////////////////
return (returnRecord);
}
}

View File

@ -23,33 +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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -58,7 +68,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOutput>
{
private static final Logger LOG = LogManager.getLogger(InsertAction.class);
private static final QLogger LOG = QLogger.getLogger(InsertAction.class);
@ -71,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);
@ -96,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);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -117,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,14 +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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
/*******************************************************************************
@ -47,7 +61,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QueryAction
{
private static final Logger LOG = LogManager.getLogger(QueryAction.class);
private static final QLogger LOG = QLogger.getLogger(QueryAction.class);
private Optional<AbstractPostQueryCustomizer> postQueryRecordCustomizer;
@ -71,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?
@ -87,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,14 +54,13 @@ 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(actionInput.getInstance());
queryInput.setSession(actionInput.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
queryInput.setTransaction(transaction);
@ -115,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

@ -82,8 +82,7 @@ public class RenderTemplateAction extends AbstractQActionFunction<RenderTemplate
*******************************************************************************/
public static String render(AbstractActionInput parentActionInput, TemplateType templateType, Map<String, Object> context, String code) throws QException
{
RenderTemplateInput renderTemplateInput = new RenderTemplateInput(parentActionInput.getInstance());
renderTemplateInput.setSession(parentActionInput.getSession());
RenderTemplateInput renderTemplateInput = new RenderTemplateInput();
renderTemplateInput.setCode(code);
renderTemplateInput.setContext(context);
renderTemplateInput.setTemplateType(templateType);

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,7 +34,9 @@ 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;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -55,8 +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 org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -65,16 +66,25 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QPossibleValueTranslator
{
private static final Logger LOG = LogManager.getLogger(QPossibleValueTranslator.class);
private final QInstance qInstance;
private final QSession session;
private static final QLogger LOG = QLogger.getLogger(QPossibleValueTranslator.class);
///////////////////////////////////////////////////////
// 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()
{
}
@ -83,10 +93,6 @@ public class QPossibleValueTranslator
*******************************************************************************/
public QPossibleValueTranslator(QInstance qInstance, QSession session)
{
this.qInstance = qInstance;
this.session = session;
this.possibleValueCache = new HashMap<>();
}
@ -139,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();
@ -150,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;
@ -210,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() + "]");
@ -412,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));
}
}
@ -463,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()))
@ -499,12 +514,11 @@ 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))
{
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setSession(session);
QueryInput queryInput = new QueryInput();
queryInput.setTableName(tableName);
queryInput.setFilter(new QQueryFilter().withCriteria(new QFilterCriteria(primaryKeyField, QCriteriaOperator.IN, page)));
@ -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,18 +25,18 @@ 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;
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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -45,10 +45,12 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QValueFormatter
{
private static final Logger LOG = LogManager.getLogger(QValueFormatter.class);
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");
@ -57,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));
}
@ -103,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 //
////////////////////////////////////////////////////////
@ -169,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.
*******************************************************************************/
@ -261,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)
{
@ -279,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
*******************************************************************************/
@ -294,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,8 +26,10 @@ 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;
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;
@ -42,11 +44,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import org.apache.commons.lang.NotImplementedException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -55,7 +56,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class SearchPossibleValueSourceAction
{
private static final Logger LOG = LogManager.getLogger(SearchPossibleValueSourceAction.class);
private static final QLogger LOG = QLogger.getLogger(SearchPossibleValueSourceAction.class);
private QPossibleValueTranslator possibleValueTranslator;
@ -105,13 +106,15 @@ public class SearchPossibleValueSourceAction
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
List<Serializable> matchingIds = new ArrayList<>();
List<?> inputIdsAsCorrectType = convertInputIdsToEnumIdType(possibleValueSource, input.getIdList());
for(QPossibleValue<?> possibleValue : possibleValueSource.getEnumValues())
{
boolean match = false;
if(input.getIdList() != null)
{
if(input.getIdList().contains(possibleValue.getId()))
if(inputIdsAsCorrectType.contains(possibleValue.getId()))
{
match = true;
}
@ -146,6 +149,44 @@ public class SearchPossibleValueSourceAction
/*******************************************************************************
** 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 at the first id in the enum, and then
** maps all the inputIds to be of the same type.
*******************************************************************************/
private List<Object> convertInputIdsToEnumIdType(QPossibleValueSource possibleValueSource, List<Serializable> inputIdList)
{
List<Object> rs = new ArrayList<>();
if(CollectionUtils.nullSafeIsEmpty(inputIdList))
{
return (rs);
}
Object anIdFromTheEnum = possibleValueSource.getEnumValues().get(0).getId();
if(anIdFromTheEnum instanceof Integer)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsInteger(id)));
}
else if(anIdFromTheEnum instanceof String)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsString(id)));
}
else if(anIdFromTheEnum instanceof Boolean)
{
inputIdList.forEach(id -> rs.add(ValueUtils.getValueAsBoolean(id)));
}
else
{
LOG.warn("Unexpected type [" + anIdFromTheEnum.getClass().getSimpleName() + "] for ids in enum: " + possibleValueSource.getName());
}
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
@ -153,8 +194,7 @@ public class SearchPossibleValueSourceAction
{
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceOutput();
QueryInput queryInput = new QueryInput(input.getInstance());
queryInput.setSession(input.getSession());
QueryInput queryInput = new QueryInput();
queryInput.setTableName(possibleValueSource.getTableName());
QTableMetaData table = input.getInstance().getTable(possibleValueSource.getTableName());
@ -236,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

@ -0,0 +1,38 @@
/*
* 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.context;
import java.util.Stack;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** record containing the values managed by QContext.
*******************************************************************************/
public record CapturedContext(QInstance qInstance, QSession qSession, QBackendTransaction qBackendTransaction, Stack<AbstractActionInput> actionStack)
{
}

View File

@ -0,0 +1,262 @@
/*
* 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.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;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** A collection of thread-local variables, to define the current context of the
** QQQ code that is running. e.g., what QInstance is being used, what QSession
** is active, etc.
*******************************************************************************/
public class QContext
{
private static final QLogger LOG = QLogger.getLogger(QContext.class);
private static ThreadLocal<QInstance> qInstanceThreadLocal = new ThreadLocal<>();
private static ThreadLocal<QSession> qSessionThreadLocal = new ThreadLocal<>();
private static ThreadLocal<QBackendTransaction> qBackendTransactionThreadLocal = new ThreadLocal<>();
private static ThreadLocal<Stack<AbstractActionInput>> actionStackThreadLocal = new ThreadLocal<>();
/*******************************************************************************
** private constructor - class is not meant to be instantiated.
*******************************************************************************/
private QContext()
{
}
/*******************************************************************************
** Most common method to set or init the context - e.g., set the current thread
** with a QInstance and QSession.
*******************************************************************************/
public static void init(QInstance qInstance, QSession qSession)
{
init(qInstance, qSession, null, null);
}
/*******************************************************************************
** Full flavor init method - also take a transaction and action input (to seed the stack).
*******************************************************************************/
public static void init(QInstance qInstance, QSession qSession, QBackendTransaction transaction, AbstractActionInput actionInput)
{
qInstanceThreadLocal.set(qInstance);
qSessionThreadLocal.set(qSession);
qBackendTransactionThreadLocal.set(transaction);
actionStackThreadLocal.set(new Stack<>());
if(actionInput != null)
{
actionStackThreadLocal.get().add(actionInput);
}
if(!qInstance.getHasBeenValidated())
{
try
{
new QInstanceValidator().validate(qInstance);
}
catch(QInstanceValidationException e)
{
LOG.warn(e);
throw (new IllegalArgumentException("QInstance failed validation" + e.getMessage(), e));
}
}
}
/*******************************************************************************
** Init a new thread with the context captured from a different thread. e.g.,
** when starting some async task.
*******************************************************************************/
public static void init(CapturedContext capturedContext)
{
init(capturedContext.qInstance(), capturedContext.qSession(), capturedContext.qBackendTransaction(), null);
actionStackThreadLocal.set(capturedContext.actionStack());
}
/*******************************************************************************
** Capture all values from the current thread - meant to be used with the init
** overload that takes a CapturedContext, for setting up a child thread.
*******************************************************************************/
public static CapturedContext capture()
{
return (new CapturedContext(getQInstance(), getQSession(), getQBackendTransaction(), getActionStack()));
}
/*******************************************************************************
** Clear all values in the current thread.
*******************************************************************************/
public static void clear()
{
qInstanceThreadLocal.remove();
qSessionThreadLocal.remove();
qBackendTransactionThreadLocal.remove();
actionStackThreadLocal.remove();
}
/*******************************************************************************
**
*******************************************************************************/
public static QInstance getQInstance()
{
return (qInstanceThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static QSession getQSession()
{
return (qSessionThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static QBackendTransaction getQBackendTransaction()
{
return (qBackendTransactionThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static Stack<AbstractActionInput> getActionStack()
{
return (actionStackThreadLocal.get());
}
/*******************************************************************************
**
*******************************************************************************/
public static void pushAction(AbstractActionInput action)
{
if(actionStackThreadLocal.get() == null)
{
actionStackThreadLocal.set(new Stack<>());
}
actionStackThreadLocal.get().push(action);
}
/*******************************************************************************
**
*******************************************************************************/
public static void popAction()
{
try
{
actionStackThreadLocal.get().pop();
}
catch(Exception e)
{
LOG.debug("Error popping action stack", e);
}
}
/*******************************************************************************
**
*******************************************************************************/
public static void setQInstance(QInstance qInstance)
{
qInstanceThreadLocal.set(qInstance);
}
/*******************************************************************************
**
*******************************************************************************/
public static void setQSession(QSession qSession)
{
qSessionThreadLocal.set(qSession);
}
/*******************************************************************************
**
*******************************************************************************/
public static void setTransaction(QBackendTransaction transaction)
{
qBackendTransactionThreadLocal.set(transaction);
}
/*******************************************************************************
**
*******************************************************************************/
public static void clearTransaction()
{
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

@ -33,6 +33,7 @@ import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
@ -59,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;
@ -72,8 +74,6 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -83,7 +83,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QInstanceEnricher
{
private static final Logger LOG = LogManager.getLogger(QInstanceEnricher.class);
private static final QLogger LOG = QLogger.getLogger(QInstanceEnricher.class);
private final QInstance qInstance;
@ -181,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()))
@ -198,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());
}
}
@ -313,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);
}
//////////////////////////////////////////////////////////////////////////
@ -328,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()))
@ -341,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));
@ -354,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;
@ -39,12 +40,14 @@ import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
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.tables.query.QFilterOrderBy;
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;
@ -65,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;
@ -76,8 +80,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -92,7 +94,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QInstanceValidator
{
private static final Logger LOG = LogManager.getLogger(QInstanceValidator.class);
private static final QLogger LOG = QLogger.getLogger(QInstanceValidator.class);
private boolean printWarnings = false;
@ -142,6 +144,7 @@ public class QInstanceValidator
validateQueuesAndProviders(qInstance);
validateJoins(qInstance);
validateSecurityKeyTypes(qInstance);
validateMiddlewareMetaData(qInstance);
validateUniqueTopLevelNames(qInstance);
}
@ -160,6 +163,19 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateMiddlewareMetaData(QInstance qInstance)
{
for(QMiddlewareInstanceMetaData middlewareInstanceMetaData : CollectionUtils.nonNullMap(qInstance.getMiddlewareMetaData()).values())
{
middlewareInstanceMetaData.validate(qInstance, this);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -442,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);
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -455,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();
@ -481,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");
@ -888,7 +971,8 @@ public class QInstanceValidator
//////////////////////////////////////////
// otherwise, just append the exception //
//////////////////////////////////////////
errors.add(prefix + ": " + e);
e.printStackTrace();
errors.add(prefix + ": " + e.getMessage());
}
}
}

View File

@ -29,11 +29,10 @@ import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -49,7 +48,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class QMetaDataVariableInterpreter
{
private static final Logger LOG = LogManager.getLogger(QMetaDataVariableInterpreter.class);
private static final QLogger LOG = QLogger.getLogger(QMetaDataVariableInterpreter.class);
private Map<String, String> environmentOverrides;
private Map<String, Map<String, Serializable>> valueMaps;

View File

@ -0,0 +1,275 @@
/*
* 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.instances;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder;
import com.amazonaws.services.secretsmanager.model.CreateSecretRequest;
import com.amazonaws.services.secretsmanager.model.Filter;
import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest;
import com.amazonaws.services.secretsmanager.model.GetSecretValueResult;
import com.amazonaws.services.secretsmanager.model.ListSecretsRequest;
import com.amazonaws.services.secretsmanager.model.ListSecretsResult;
import com.amazonaws.services.secretsmanager.model.PutSecretValueRequest;
import com.amazonaws.services.secretsmanager.model.ResourceExistsException;
import com.amazonaws.services.secretsmanager.model.SecretListEntry;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.json.JSONException;
import org.json.JSONObject;
/*******************************************************************************
** Utility class for working with AWS Secrets Manager.
**
** Relies on environment variables:
** SECRETS_MANAGER_ACCESS_KEY
** SECRETS_MANAGER_SECRET_KEY
** SECRETS_MANAGER_REGION
**
*******************************************************************************/
public class SecretsManagerUtils
{
private static final Logger LOG = LogManager.getLogger(SecretsManagerUtils.class);
private static QMetaDataVariableInterpreter qMetaDataVariableInterpreter;
private static AWSSecretsManager _client = null;
/*******************************************************************************
** IF secret manager ENV vars are set,
** THEN lookup all secrets starting with the given prefix,
** and write them to a .env file (backing up any pre-existing .env files first).
*******************************************************************************/
public static void writeEnvFromSecretsWithNamePrefix(String prefix) throws IOException
{
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
if(optionalSecretsManagerClient.isPresent())
{
AWSSecretsManager client = optionalSecretsManagerClient.get();
ListSecretsRequest listSecretsRequest = new ListSecretsRequest().withFilters(new Filter().withKey("name").withValues(prefix));
listSecretsRequest.withMaxResults(100);
ListSecretsResult listSecretsResult = client.listSecrets(listSecretsRequest);
StringBuilder fullEnv = new StringBuilder();
while(true)
{
for(SecretListEntry secretListEntry : listSecretsResult.getSecretList())
{
String nameWithoutPrefix = secretListEntry.getName().replace(prefix, "");
Optional<String> secretValue = getSecret(prefix, nameWithoutPrefix);
if(secretValue.isPresent())
{
String envLine = nameWithoutPrefix + "=" + secretValue.get();
fullEnv.append(envLine).append('\n');
}
}
if(listSecretsResult.getNextToken() != null)
{
LOG.trace("Calling for next token...");
listSecretsRequest.setNextToken(listSecretsResult.getNextToken());
listSecretsResult = client.listSecrets(listSecretsRequest);
}
else
{
break;
}
}
File dotEnv = new File(".env");
if(dotEnv.exists())
{
dotEnv.renameTo(new File(".env.backup-" + System.currentTimeMillis()));
}
FileUtils.writeStringToFile(dotEnv, fullEnv.toString());
}
else
{
LOG.info("Not writing .env from secrets manager");
}
}
/*******************************************************************************
** Get a single secret value.
**
** The lookup in secrets manager is done by (path + name). Then, in the value
** that comes back, if it looks like JSON, we look for a value inside it under
** the key of just "name". Else, if we didn't get JSON back, then we just return
** the full text value of the secret.
*******************************************************************************/
public static Optional<String> getSecret(String path, String name)
{
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
if(optionalSecretsManagerClient.isPresent())
{
try
{
AWSSecretsManager client = optionalSecretsManagerClient.get();
String secretId = path + name;
GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest().withSecretId(secretId);
GetSecretValueResult getSecretValueResult = client.getSecretValue(getSecretValueRequest);
try
{
JSONObject secretJSON = new JSONObject(getSecretValueResult.getSecretString());
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// know we know it's a json object - so - commit to either returning the value under this name, else warning and returning empty //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(secretJSON.has(name))
{
return (Optional.of(secretJSON.getString(name)));
}
else
{
LOG.warn("SecretsManager secret at [" + secretId + "] was a JSON object, but it did not contain a key of [" + name + "] - so returning empty.");
return (Optional.empty());
}
}
catch(JSONException je)
{
//////////////////////////////////////////////////////////////////////////////////////////////////
// if the secret value couldn't be parsed as json, then assume it to be text and just return it //
//////////////////////////////////////////////////////////////////////////////////////////////////
return (Optional.of(getSecretValueResult.getSecretString()));
}
}
catch(Exception e)
{
LOG.debug("Error getting secret from secretManager: ", e);
}
}
return (Optional.empty());
}
/*******************************************************************************
** Tries to do a Create - if that fails, then does a Put (update).
**
** Path is expected to end in a /, but I suppose it isn't strictly required.
*******************************************************************************/
public static void writeSecret(String path, String name, String value)
{
JSONObject secretJson = new JSONObject();
secretJson.put(name, value);
Optional<AWSSecretsManager> optionalSecretsManagerClient = getSecretsManagerClient();
if(optionalSecretsManagerClient.isPresent())
{
AWSSecretsManager client = optionalSecretsManagerClient.get();
try
{
CreateSecretRequest createSecretRequest = new CreateSecretRequest();
createSecretRequest.setName(path + name);
createSecretRequest.setSecretString(secretJson.toString());
client.createSecret(createSecretRequest);
}
catch(ResourceExistsException e)
{
PutSecretValueRequest putSecretValueRequest = new PutSecretValueRequest();
putSecretValueRequest.setSecretId(path + name);
putSecretValueRequest.setSecretString(secretJson.toString());
client.putSecretValue(putSecretValueRequest);
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private static Optional<AWSSecretsManager> getSecretsManagerClient()
{
if(_client == null)
{
QMetaDataVariableInterpreter interpreter = getQMetaDataVariableInterpreter();
String accessKey = interpreter.interpret("${env.SECRETS_MANAGER_ACCESS_KEY}");
String secretKey = interpreter.interpret("${env.SECRETS_MANAGER_SECRET_KEY}");
String region = interpreter.interpret("${env.SECRETS_MANAGER_REGION}");
if(StringUtils.hasContent(accessKey) && StringUtils.hasContent(secretKey) && StringUtils.hasContent(region))
{
try
{
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
_client = AWSSecretsManagerClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region)
.build();
}
catch(Exception e)
{
LOG.error("Error opening Secrets Manager client", e);
}
}
else
{
LOG.warn("One or more SECRETS_MANAGER env var was not set. Unable to open Secrets Manager client.");
}
}
return (Optional.ofNullable(_client));
}
/*******************************************************************************
**
*******************************************************************************/
private static QMetaDataVariableInterpreter getQMetaDataVariableInterpreter()
{
return Objects.requireNonNullElseGet(qMetaDataVariableInterpreter, QMetaDataVariableInterpreter::new);
}
/*******************************************************************************
** Ideally meant for tests or one-offs to set up a variable interpreter with
** an override ENV.
*******************************************************************************/
static void setQMetaDataVariableInterpreter(QMetaDataVariableInterpreter qMetaDataVariableInterpreter)
{
SecretsManagerUtils.qMetaDataVariableInterpreter = qMetaDataVariableInterpreter;
}
}

View File

@ -0,0 +1,170 @@
/*
* 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.logging;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
/*******************************************************************************
**
*******************************************************************************/
public class LogPair
{
private String key;
private Object value;
/*******************************************************************************
** Constructor
**
*******************************************************************************/
public LogPair(String key, Object value)
{
this.key = key;
this.value = value;
}
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
String valueString = getValueString(value);
return "\"" + Objects.requireNonNullElse(key, "null").replace('"', '.') + "\":" + valueString;
}
/*******************************************************************************
**
*******************************************************************************/
private String getValueString(Object value)
{
String valueString;
if(value == null)
{
valueString = "null";
}
else if(value instanceof LogPair subLogPair)
{
valueString = '{' + subLogPair.toString() + '}';
}
else if(value instanceof LogPair[] subLogPairs)
{
String subLogPairsString = Arrays.stream(subLogPairs).map(LogPair::toString).collect(Collectors.joining(","));
valueString = '{' + subLogPairsString + '}';
}
else if(value instanceof UnsafeSupplier<?, ?> us)
{
try
{
Object o = us.get();
return getValueString(o);
}
catch(Exception e)
{
valueString = "LogValueError";
}
}
else if(value instanceof Number n)
{
valueString = String.valueOf(n);
}
else
{
valueString = '"' + String.valueOf(value).replace("\"", "\\\"") + '"';
}
return valueString;
}
/*******************************************************************************
** Getter for key
*******************************************************************************/
public String getKey()
{
return (this.key);
}
/*******************************************************************************
** Setter for key
*******************************************************************************/
public void setKey(String key)
{
this.key = key;
}
/*******************************************************************************
** Fluent setter for key
*******************************************************************************/
public LogPair withKey(String key)
{
this.key = key;
return (this);
}
/*******************************************************************************
** Getter for value
*******************************************************************************/
public Object getValue()
{
return (this.value);
}
/*******************************************************************************
** Setter for value
*******************************************************************************/
public void setValue(Object value)
{
this.value = value;
}
/*******************************************************************************
** Fluent setter for value
*******************************************************************************/
public LogPair withValue(Object value)
{
this.value = value;
return (this);
}
}

View File

@ -0,0 +1,197 @@
/*
* 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.logging;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import com.kingsrook.qqq.backend.core.utils.lambdas.UnsafeSupplier;
/*******************************************************************************
**
*******************************************************************************/
public class LogUtils
{
/*******************************************************************************
**
*******************************************************************************/
public static String jsonLog(List<LogPair> logPairs)
{
List<LogPair> filteredList = logPairs.stream().filter(Objects::nonNull).toList();
if(filteredList.isEmpty())
{
if(QLogger.processTagLogPairJson != null)
{
return ("{" + QLogger.processTagLogPairJson + "}");
}
else
{
return ("{}");
}
}
return ('{' + filteredList.stream().map(LogPair::toString).collect(Collectors.joining(","))
+ (QLogger.processTagLogPairJson != null ? (',' + QLogger.processTagLogPairJson) : "") + '}');
}
/*******************************************************************************
**
*******************************************************************************/
public static String jsonLog(LogPair... logPairs)
{
if(logPairs == null || logPairs.length == 0)
{
return ("{}");
}
return (jsonLog(Arrays.asList(logPairs)));
}
/*******************************************************************************
**
*******************************************************************************/
public static LogPair logPair(String key, Object value)
{
return (new LogPair(key, value));
}
/*******************************************************************************
**
*******************************************************************************/
public static LogPair logPair(String key, UnsafeSupplier<Object, Exception> valueSupplier)
{
try
{
return (new LogPair(key, valueSupplier.get()));
}
catch(Exception e)
{
return (new LogPair(key, "exceptionLoggingValue: " + e.getMessage()));
}
}
/*******************************************************************************
**
*******************************************************************************/
public static LogPair logPair(String key, LogPair... values)
{
return (new LogPair(key, values));
}
/*******************************************************************************
**
*******************************************************************************/
static String filterStackTrace(String stackTrace)
{
try
{
String packagesToKeep = "com.kingsrook|com.nutrifresh"; // todo - parameterize!!
StringBuilder rs = new StringBuilder();
String[] lines = stackTrace.split("\n");
int indexWithinSubStack = 0;
int skipsInThisPackage = 0;
String packageBeingSkipped = null;
for(String line : lines)
{
boolean keepLine = true;
if(line.matches("^\\s+at .*"))
{
keepLine = false;
indexWithinSubStack++;
if(line.matches("^\\s+at (" + packagesToKeep + ").*"))
{
keepLine = true;
}
if(indexWithinSubStack == 1)
{
keepLine = true;
}
}
else
{
indexWithinSubStack = 0;
if(skipsInThisPackage > 0)
{
rs.append("\t... ").append(skipsInThisPackage).append(" in ").append(packageBeingSkipped).append("\n");
skipsInThisPackage = 0;
}
}
if(keepLine)
{
rs.append(line).append("\n");
}
else
{
String thisPackage = line.replaceFirst("\\s+at ", "").replaceFirst("(\\w+\\.\\w+).*", "$1");
if(Objects.equals(thisPackage, packageBeingSkipped))
{
skipsInThisPackage++;
}
else
{
if(skipsInThisPackage > 0)
{
rs.append("\t... ").append(skipsInThisPackage).append(" in ").append(packageBeingSkipped).append("\n");
}
skipsInThisPackage = 1;
}
packageBeingSkipped = thisPackage;
}
}
if(rs.length() > 0)
{
rs.deleteCharAt(rs.length() - 1);
}
return (rs.toString());
}
catch(Exception e)
{
e.printStackTrace();
///////////////////////////////////////////////
// upon any exception, just return the input //
///////////////////////////////////////////////
return (stackTrace);
}
}
}

View File

@ -0,0 +1,575 @@
/*
* 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.logging;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
** Wrapper for
**
*******************************************************************************/
public class QLogger
{
private static Map<String, QLogger> loggerMap = Collections.synchronizedMap(new HashMap<>());
private static boolean logSessionIdEnabled = true;
//////////////////////////////////////////////////////////////////////
// note - read in LogUtils, where log pairs are made into a string. //
//////////////////////////////////////////////////////////////////////
static String processTagLogPairJson = null;
private Logger logger;
static
{
//////////////////////////////////////////////////////////////////////////////////////////////
// read the property to see if sessionIds in log messages is enabled, just once, statically //
//////////////////////////////////////////////////////////////////////////////////////////////
try
{
String propertyName = "qqq.logger.logSessionId.disabled";
String propertyValue = System.getProperty(propertyName, "");
if(propertyValue.equals("true"))
{
logSessionIdEnabled = false;
}
}
catch(Exception e)
{
e.printStackTrace();
}
////////////////////////////////////////////////////////////////////////////////////////////
// read the property (or env var) to see if there's a "processTag" to put on all messages //
////////////////////////////////////////////////////////////////////////////////////////////
try
{
String processTag = System.getProperty("qqq.logger.processTag");
if(processTag == null)
{
processTag = new QMetaDataVariableInterpreter().interpret("${env.QQQ_LOGGER_PROCESS_TAG}");
}
if(StringUtils.hasContent(processTag))
{
processTagLogPairJson = "\"processTag\":\"" + processTag + "\"";
}
}
catch(Exception e)
{
e.printStackTrace();
}
}
/*******************************************************************************
**
*******************************************************************************/
public QLogger(Logger logger)
{
this.logger = logger;
}
/*******************************************************************************
**
*******************************************************************************/
public static QLogger getLogger(Class<?> c)
{
return (loggerMap.computeIfAbsent(c.getName(), x -> new QLogger(LogManager.getLogger(c))));
}
/*******************************************************************************
**
*******************************************************************************/
public void log(Level level, String message)
{
logger.log(level, makeJsonString(message));
}
/*******************************************************************************
**
*******************************************************************************/
public void log(Level level, String message, Throwable t)
{
logger.log(level, makeJsonString(message, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void log(Level level, Throwable t)
{
logger.log(level, makeJsonString(null, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void trace(String message)
{
logger.trace(makeJsonString(message));
}
/*******************************************************************************
**
*******************************************************************************/
public void trace(String message, LogPair... logPairs)
{
logger.trace(makeJsonString(message, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void trace(String message, Object... values)
{
logger.trace(makeJsonString(message), values);
}
/*******************************************************************************
**
*******************************************************************************/
public void trace(String message, Throwable t)
{
logger.trace(makeJsonString(message, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void trace(String message, Throwable t, LogPair... logPairs)
{
logger.trace(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void trace(Throwable t)
{
logger.trace(makeJsonString(null, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void debug(String message)
{
logger.debug(makeJsonString(message));
}
/*******************************************************************************
**
*******************************************************************************/
public void debug(String message, LogPair... logPairs)
{
logger.debug(makeJsonString(message, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void debug(String message, Object... values)
{
logger.debug(makeJsonString(message), values);
}
/*******************************************************************************
**
*******************************************************************************/
public void debug(String message, Throwable t)
{
logger.debug(makeJsonString(message, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void debug(String message, Throwable t, LogPair... logPairs)
{
logger.debug(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void debug(Throwable t)
{
logger.debug(makeJsonString(null, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(String message)
{
logger.info(makeJsonString(message));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(LogPair... logPairs)
{
logger.info(makeJsonString(null, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(List<LogPair> logPairList)
{
logger.info(makeJsonString(null, null, logPairList));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(String message, LogPair... logPairs)
{
logger.info(makeJsonString(message, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(String message, Object... values)
{
logger.info(makeJsonString(message), values);
}
/*******************************************************************************
**
*******************************************************************************/
public void info(String message, Throwable t)
{
logger.info(makeJsonString(message, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(String message, Throwable t, LogPair... logPairs)
{
logger.info(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void info(Throwable t)
{
logger.info(makeJsonString(null, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(String message)
{
logger.warn(makeJsonString(message));
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(String message, LogPair... logPairs)
{
logger.warn(makeJsonString(message, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(String message, Object... values)
{
logger.warn(makeJsonString(message), values);
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(String message, Throwable t)
{
logger.warn(makeJsonString(message, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(String message, Throwable t, LogPair... logPairs)
{
logger.warn(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(Throwable t)
{
logger.warn(makeJsonString(null, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void error(String message)
{
logger.error(makeJsonString(message));
}
/*******************************************************************************
**
*******************************************************************************/
public void error(String message, LogPair... logPairs)
{
logger.error(makeJsonString(message, null, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void error(String message, Object... values)
{
logger.error(makeJsonString(message), values);
}
/*******************************************************************************
**
*******************************************************************************/
public void error(String message, Throwable t)
{
logger.error(makeJsonString(message, t));
}
/*******************************************************************************
**
*******************************************************************************/
public void error(String message, Throwable t, LogPair... logPairs)
{
logger.error(makeJsonString(message, t, logPairs));
}
/*******************************************************************************
**
*******************************************************************************/
public void error(Throwable t)
{
logger.error(makeJsonString(null, t));
}
/*******************************************************************************
**
*******************************************************************************/
private String makeJsonString(String message)
{
return (makeJsonString(message, null));
}
/*******************************************************************************
**
*******************************************************************************/
private String makeJsonString(String message, Throwable t)
{
return (makeJsonString(message, t, (List<LogPair>) null));
}
/*******************************************************************************
**
*******************************************************************************/
private String makeJsonString(String message, Throwable t, LogPair[] logPairs)
{
List<LogPair> logPairList = new ArrayList<>();
if(logPairs != null)
{
logPairList.addAll(Arrays.stream(logPairs).toList());
}
return (makeJsonString(message, t, logPairList));
}
/*******************************************************************************
**
*******************************************************************************/
private String makeJsonString(String message, Throwable t, List<LogPair> logPairList)
{
if(logPairList == null)
{
logPairList = new ArrayList<>();
}
if(StringUtils.hasContent(message))
{
logPairList.add(0, logPair("message", message));
}
addSessionLogPair(logPairList);
if(t != null)
{
logPairList.add(logPair("stackTrace", LogUtils.filterStackTrace(ExceptionUtils.getStackTrace(t))));
}
return (LogUtils.jsonLog(logPairList));
}
/*******************************************************************************
**
*******************************************************************************/
private static void addSessionLogPair(List<LogPair> logPairList)
{
if(logSessionIdEnabled)
{
QSession session = QContext.getQSession();
LogPair sessionLogPair;
if(session == null)
{
///////////////////////////////////////////////////////////////////////////////////////////////////
// note - being careful here to make the same json structure whether session is known or unknown //
// (e.g., not a string in one case and an object in another case) - to help loggly. //
///////////////////////////////////////////////////////////////////////////////////////////////////
sessionLogPair = logPair("session", logPair("id", "unknown"));
}
else
{
String user = "unknown";
if(session.getUser() != null)
{
user = session.getUser().getIdReference();
}
sessionLogPair = logPair("session", logPair("id", session.getUuid()), logPair("user", user));
}
try
{
logPairList.add(sessionLogPair);
}
catch(Exception e)
{
//////////////////////////////////////
// deal with not-modifiable list... //
//////////////////////////////////////
logPairList = new ArrayList<>(logPairList);
logPairList.add(sessionLogPair);
}
}
}
}

View File

@ -23,15 +23,16 @@ 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;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.logging.QLogger;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/*******************************************************************************
@ -40,10 +41,7 @@ import org.apache.logging.log4j.Logger;
*******************************************************************************/
public class AbstractActionInput
{
private static final Logger LOG = LogManager.getLogger(AbstractActionInput.class);
protected QInstance instance;
protected QSession session;
private static final QLogger LOG = QLogger.getLogger(AbstractActionInput.class);
private AsyncJobCallback asyncJobCallback;
@ -58,30 +56,9 @@ public class AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public AbstractActionInput(QInstance instance)
{
this.instance = instance;
validateInstance(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public AbstractActionInput(QInstance instance, QSession session)
{
this(instance);
this.session = session;
}
/*******************************************************************************
** performance instance validation (if not previously done).
* // todo - verify this is happening (e.g., when context is set i guess)
*******************************************************************************/
private void validateInstance(QInstance instance)
{
@ -108,9 +85,10 @@ public class AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
@JsonIgnore
public QAuthenticationMetaData getAuthenticationMetaData()
{
return (instance.getAuthentication());
return (getInstance().getAuthentication());
}
@ -119,21 +97,10 @@ public class AbstractActionInput
** Getter for instance
**
*******************************************************************************/
@JsonIgnore
public QInstance getInstance()
{
return instance;
}
/*******************************************************************************
** Setter for instance
**
*******************************************************************************/
public void setInstance(QInstance instance)
{
validateInstance(instance);
this.instance = instance;
return (QContext.getQInstance());
}
@ -142,20 +109,10 @@ public class AbstractActionInput
** Getter for session
**
*******************************************************************************/
@JsonIgnore
public QSession getSession()
{
return session;
}
/*******************************************************************************
** Setter for session
**
*******************************************************************************/
public void setSession(QSession session)
{
this.session = session;
return (QContext.getQSession());
}
@ -164,6 +121,7 @@ public class AbstractActionInput
** Getter for asyncJobCallback
**
*******************************************************************************/
@JsonIgnore
public AsyncJobCallback getAsyncJobCallback()
{
if(asyncJobCallback == null)
@ -194,18 +152,6 @@ public class AbstractActionInput
*******************************************************************************/
public AbstractActionInput withInstance(QInstance instance)
{
this.instance = instance;
return (this);
}
/*******************************************************************************
** Fluent setter for session
*******************************************************************************/
public AbstractActionInput withSession(QSession session)
{
this.session = session;
return (this);
}

View File

@ -22,10 +22,9 @@
package com.kingsrook.qqq.backend.core.model.actions;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
@ -38,26 +37,6 @@ public class AbstractTableActionInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public QBackendMetaData getBackend()
{
return (instance.getBackendForTable(getTableName()));
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData getTable()
{
return (instance.getTable(getTableName()));
}
/*******************************************************************************
**
*******************************************************************************/
@ -70,9 +49,19 @@ public class AbstractTableActionInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public AbstractTableActionInput(QInstance instance)
public QBackendMetaData getBackend()
{
super(instance);
return (QContext.getQInstance().getBackendForTable(getTableName()));
}
/*******************************************************************************
**
*******************************************************************************/
public QTableMetaData getTable()
{
return (QContext.getQInstance().getTable(getTableName()));
}
@ -108,16 +97,4 @@ public class AbstractTableActionInput extends AbstractActionInput
return (this);
}
/*******************************************************************************
** Fluent setter for session
*******************************************************************************/
@Override
public AbstractTableActionInput withSession(QSession session)
{
super.withSession(session);
return (this);
}
}

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.model.actions.audits;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
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 implements Serializable
{
private List<AuditSingleInput> auditSingleInputList = new ArrayList<>();
/*******************************************************************************
** Getter for auditSingleInputList
*******************************************************************************/
public List<AuditSingleInput> getAuditSingleInputList()
{
return (this.auditSingleInputList);
}
/*******************************************************************************
** Setter for auditSingleInputList
*******************************************************************************/
public void setAuditSingleInputList(List<AuditSingleInput> auditSingleInputList)
{
this.auditSingleInputList = auditSingleInputList;
}
/*******************************************************************************
** Fluent setter for auditSingleInputList
*******************************************************************************/
public AuditInput withAuditSingleInputList(List<AuditSingleInput> auditSingleInputList)
{
this.auditSingleInputList = auditSingleInputList;
return (this);
}
/*******************************************************************************
** Add a single auditSingleInput
*******************************************************************************/
public void addAuditSingleInput(AuditSingleInput auditSingleInput)
{
if(this.auditSingleInputList == null)
{
this.auditSingleInputList = new ArrayList<>();
}
this.auditSingleInputList.add(auditSingleInput);
}
/*******************************************************************************
** Fluent setter to add a single auditSingleInput
*******************************************************************************/
public AuditInput withAuditSingleInput(AuditSingleInput auditSingleInput)
{
addAuditSingleInput(auditSingleInput);
return (this);
}
}

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.actions.audits;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionOutput;
/*******************************************************************************
**
*******************************************************************************/
public class AuditOutput extends AbstractActionOutput
{
}

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

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
@ -40,14 +39,4 @@ public class MetaDataInput extends AbstractActionInput
{
}
/*******************************************************************************
**
*******************************************************************************/
public MetaDataInput(QInstance instance)
{
super(instance);
}
}

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
@ -45,16 +44,6 @@ public class ProcessMetaDataInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public ProcessMetaDataInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for processName
**

View File

@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
@ -40,14 +39,4 @@ public class TableMetaDataInput extends AbstractTableActionInput
{
}
/*******************************************************************************
**
*******************************************************************************/
public TableMetaDataInput(QInstance instance)
{
super(instance);
}
}

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

@ -25,7 +25,9 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -422,6 +424,17 @@ public class ProcessSummaryLine implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public LogPair toLogPair()
{
return (logPair("ProcessSummary", logPair("status", status), logPair("count", count), logPair("message", message)));
}
/*******************************************************************************
** Getter for messageSuffix
**
@ -454,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

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.logging.LogPair;
/*******************************************************************************
@ -47,4 +48,8 @@ public interface ProcessSummaryLineInterface extends Serializable
}
/*******************************************************************************
**
*******************************************************************************/
LogPair toLogPair();
}

View File

@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.model.actions.processes;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.logging.LogPair;
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
/*******************************************************************************
@ -50,6 +52,18 @@ public class ProcessSummaryRecordLink implements ProcessSummaryLineInterface
/*******************************************************************************
**
*******************************************************************************/
@Override
public LogPair toLogPair()
{
return (logPair("ProcessSummary", logPair("status", status), logPair("tableName", tableName), logPair("recordId", recordId),
logPair("linkPreText", linkPreText), logPair("linkText", linkText), logPair("linkPostText", linkPostText)));
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -31,9 +31,9 @@ import java.util.UUID;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -73,20 +73,8 @@ public class RunBackendStepInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public RunBackendStepInput(QInstance instance)
public RunBackendStepInput(ProcessState processState)
{
super(instance);
processState = new ProcessState();
}
/*******************************************************************************
**
*******************************************************************************/
public RunBackendStepInput(QInstance instance, ProcessState processState)
{
super(instance);
this.processState = processState;
}
@ -102,7 +90,6 @@ public class RunBackendStepInput extends AbstractActionInput
public void cloneFieldsInto(RunBackendStepInput target)
{
target.setStepName(getStepName());
target.setSession(getSession());
target.setTableName(getTableName());
target.setProcessName(getProcessName());
target.setAsyncJobCallback(getAsyncJobCallback());
@ -117,7 +104,7 @@ public class RunBackendStepInput extends AbstractActionInput
*******************************************************************************/
public QStepMetaData getStepMetaData()
{
return (instance.getProcessStep(getProcessName(), getStepName()));
return (QContext.getQInstance().getProcessStep(getProcessName(), getStepName()));
}
@ -200,7 +187,7 @@ public class RunBackendStepInput extends AbstractActionInput
return (null);
}
return (instance.getTable(tableName));
return (QContext.getQInstance().getTable(tableName));
}

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

@ -27,9 +27,9 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.async.AsyncJobCallback;
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
@ -71,17 +71,6 @@ public class RunProcessInput extends AbstractActionInput
/*******************************************************************************
**
*******************************************************************************/
public RunProcessInput(QInstance instance)
{
super(instance);
processState = new ProcessState();
}
/*******************************************************************************
** e.g., for steps after the first step in a process, seed the data in a run
** function request from a process state.
@ -99,7 +88,7 @@ public class RunProcessInput extends AbstractActionInput
*******************************************************************************/
public QProcessMetaData getProcessMetaData()
{
return (instance.getProcess(getProcessName()));
return (QContext.getQInstance().getProcess(getProcessName()));
}

View File

@ -26,8 +26,6 @@ import java.io.OutputStream;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
@ -56,27 +54,6 @@ public class ExportInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public ExportInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public ExportInput(QInstance instance, QSession session)
{
super(instance);
setSession(session);
}
/*******************************************************************************
** Getter for queryFilter
**

View File

@ -27,8 +27,6 @@ import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
@ -54,27 +52,6 @@ public class ReportInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public ReportInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
**
*******************************************************************************/
public ReportInput(QInstance instance, QSession session)
{
super(instance);
setSession(session);
}
/*******************************************************************************
** Getter for reportName
**

View File

@ -27,7 +27,6 @@ import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
@ -47,9 +46,8 @@ public class ExecuteCodeInput extends AbstractActionInput
** Constructor
**
*******************************************************************************/
public ExecuteCodeInput(QInstance qInstance)
public ExecuteCodeInput()
{
super(qInstance);
}

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

@ -19,31 +19,74 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.utils;
package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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;
/*******************************************************************************
** Utility class for logging
**
*******************************************************************************/
public class QLogger
public class RunAdHocRecordScriptOutput extends AbstractActionOutput
{
private static Map<String, QLogger> loggerMap = new HashMap<>();
private Logger logger;
private Serializable output;
private QCodeExecutionLoggerInterface logger;
private Optional<Exception> exception = Optional.empty();
/*******************************************************************************
** Getter for output
**
*******************************************************************************/
public QLogger(Logger logger)
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;
}
@ -51,88 +94,43 @@ public class QLogger
/*******************************************************************************
**
** Fluent setter for logger
*******************************************************************************/
public static QLogger getLogger(Class<?> c)
public RunAdHocRecordScriptOutput withLogger(QCodeExecutionLoggerInterface logger)
{
return (loggerMap.computeIfAbsent(c.getName(), x -> new QLogger(LogManager.getLogger(c))));
this.logger = logger;
return (this);
}
/*******************************************************************************
**
** Getter for exception
*******************************************************************************/
public void debug(QSession session, String message)
public Optional<Exception> getException()
{
logger.debug(wrapMessage(session, message));
return (this.exception);
}
/*******************************************************************************
**
** Setter for exception
*******************************************************************************/
public void info(QSession session, String message)
public void setException(Optional<Exception> exception)
{
logger.info(wrapMessage(session, message));
this.exception = exception;
}
/*******************************************************************************
**
** Fluent setter for exception
*******************************************************************************/
public void warn(QSession session, String message)
public RunAdHocRecordScriptOutput withException(Optional<Exception> exception)
{
logger.warn(wrapMessage(session, message));
this.exception = exception;
return (this);
}
/*******************************************************************************
**
*******************************************************************************/
public void warn(QSession session, String message, Throwable t)
{
logger.warn(wrapMessage(session, message), t);
}
/*******************************************************************************
**
*******************************************************************************/
public void error(QSession session, String message)
{
logger.error(wrapMessage(session, message));
}
/*******************************************************************************
**
*******************************************************************************/
public void error(QSession session, String message, Throwable t)
{
logger.error(wrapMessage(session, message), t);
}
/*******************************************************************************
**
*******************************************************************************/
private String wrapMessage(QSession session, String message)
{
String propertyName = "qqq.logger.logSessionId.disabled";
String propertyValue = System.getProperty(propertyName, "");
if(propertyValue.equals("true"))
{
return (message);
}
String sessionString = (session != null) ? session.getUuid() : "Not provided";
return ("Session [" + sessionString + "] | " + message);
}
}

View File

@ -24,8 +24,8 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.scripts.logging.QCodeExecutionLoggerInterface;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.AssociatedScriptCodeReference;
@ -36,17 +36,19 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
{
private AssociatedScriptCodeReference codeReference;
private Map<String, Serializable> inputValues;
private QCodeExecutionLoggerInterface logger;
private Serializable outputObject;
private Serializable scriptUtils;
/*******************************************************************************
**
*******************************************************************************/
public RunAssociatedScriptInput(QInstance qInstance)
public RunAssociatedScriptInput()
{
super(qInstance);
}
@ -151,4 +153,56 @@ public class RunAssociatedScriptInput extends AbstractTableActionInput
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 RunAssociatedScriptInput 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;
}
}

View File

@ -24,7 +24,6 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
@ -44,9 +43,8 @@ public class StoreAssociatedScriptInput extends AbstractTableActionInput
** Constructor
**
*******************************************************************************/
public StoreAssociatedScriptInput(QInstance instance)
public StoreAssociatedScriptInput()
{
super(instance);
}

View File

@ -25,7 +25,6 @@ package com.kingsrook.qqq.backend.core.model.actions.scripts;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
@ -42,9 +41,8 @@ public class TestScriptInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public TestScriptInput(QInstance qInstance)
public TestScriptInput()
{
super(qInstance);
}

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

@ -27,7 +27,6 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
@ -39,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;
@ -53,16 +53,6 @@ public class AggregateInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public AggregateInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for filter
**
@ -245,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,7 +27,6 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
/*******************************************************************************
@ -51,16 +50,6 @@ public class CountInput extends AbstractTableActionInput
/*******************************************************************************
**
*******************************************************************************/
public CountInput(QInstance instance)
{
super(instance);
}
/*******************************************************************************
** Getter for filter
**

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